OpenTofu - the Terraform Fork

Published: Sep 28, 2023 by Isaac Johnson

If you aren’t deeply involved in spinning up Cloud Infrastructure or creating Infrastructure as Code (IaC), then perhaps you wouldn’t be aware of a rather monumental change that happened August 10th.

Hashicorp, the dominant player in the market with Terraform, an Open Source IaC framework with several professional and enterprise offerings, announced that as of the next versions of all their products, they would move off open-source by dropping the MPL in favour of a new BUSL, or Business Source License, which is ‘source ajar’ or ‘source available’ like Oracle Java or IBM Red Hat Enterprise.

First, let’s start with the fun part - getting going with OpenTofu.

Pre-req’s

First, we need Golang 1.19. This is what Terraform was based on at 1.5.x before, well, we know what they did at 1.6.

$ brew install go@1.19
Running `brew update --auto-update`...
==> Auto-updated Homebrew!
Updated 2 taps (homebrew/core and homebrew/cask).
==> New Formulae
biome                         libimobiledevice-glue         orcania                       web-ext
blake3                        libmapper                     tailwindcss                   yder
helidon                       llvm@16                       uffizzi
==> New Casks
ava                 chainner            cloudnet            expo-orbit          playdate-mirror     spundle
batteryboi          clinq               dropshelf           mutedeck            reqable             twelite-stage

You have 48 outdated formulae installed.

Warning: go@1.19 has been deprecated because it is not supported upstream!
==> Downloading https://ghcr.io/v2/homebrew/core/go/1.19/manifests/1.19.13
################################################################################################################# 100.0%
==> Fetching dependencies for go@1.19: linux-headers@5.15
==> Downloading https://ghcr.io/v2/homebrew/core/linux-headers/5.15/manifests/5.15.133
################################################################################################################# 100.0%
==> Fetching linux-headers@5.15
==> Downloading https://ghcr.io/v2/homebrew/core/linux-headers/5.15/blobs/sha256:e0a593cbe9abb95d223416df312e6fe38e69f8e
################################################################################################################# 100.0%
==> Fetching go@1.19
==> Downloading https://ghcr.io/v2/homebrew/core/go/1.19/blobs/sha256:25d23ef810a777c73e48f84ab03c7dd161d5b7706d31010f7a
################################################################################################################# 100.0%
==> Installing dependencies for go@1.19: linux-headers@5.15
==> Installing go@1.19 dependency: linux-headers@5.15
==> Downloading https://ghcr.io/v2/homebrew/core/linux-headers/5.15/manifests/5.15.133
Already downloaded: /home/builder/.cache/Homebrew/downloads/e30a76ec86e5182b4ac22c331b26ef169c54254bf3b556e9bb2c14dfd6cb6c70--linux-headers@5.15-5.15.133.bottle_manifest.json
==> Pouring linux-headers@5.15--5.15.133.x86_64_linux.bottle.tar.gz
🍺  /home/linuxbrew/.linuxbrew/Cellar/linux-headers@5.15/5.15.133: 961 files, 5.7MB
==> Installing go@1.19
==> Pouring go@1.19--1.19.13.x86_64_linux.bottle.tar.gz
==> Caveats
go@1.19 is keg-only, which means it was not symlinked into /home/linuxbrew/.linuxbrew,
because this is an alternate version of another formula.

If you need to have go@1.19 first in your PATH, run:
  echo 'export PATH="/home/linuxbrew/.linuxbrew/opt/go@1.19/bin:$PATH"' >> ~/.profile
==> Summary
🍺  /home/linuxbrew/.linuxbrew/Cellar/go@1.19/1.19.13: 12,486 files, 593MB
==> Running `brew cleanup go@1.19`...
Disable this behaviour by setting HOMEBREW_NO_INSTALL_CLEANUP.
Hide these hints with HOMEBREW_NO_ENV_HINTS (see `man brew`).
==> Caveats
==> go@1.19
go@1.19 is keg-only, which means it was not symlinked into /home/linuxbrew/.linuxbrew,
because this is an alternate version of another formula.

If you need to have go@1.19 first in your PATH, run:
  echo 'export PATH="/home/linuxbrew/.linuxbrew/opt/go@1.19/bin:$PATH"' >> ~/.profile

As suggested, I’ll add to my path

$ echo 'export PATH="/home/linuxbrew/.linuxbrew/opt/go@1.19/bin:$PATH"' >> ~/.profile

Next, we’ll clone the repo down

$ git clone https://github.com/opentofu/opentofu.git
Cloning into 'opentofu'...
remote: Enumerating objects: 261825, done.
remote: Counting objects: 100% (5392/5392), done.
remote: Compressing objects: 100% (1661/1661), done.
remote: Total 261825 (delta 4084), reused 4334 (delta 3704), pack-reused 256433
Receiving objects: 100% (261825/261825), 266.19 MiB | 10.87 MiB/s, done.
Resolving deltas: 100% (162026/162026), done.

We can now build this using Golang 1.19

builder@DESKTOP-QADGF36:~/Workspaces/opentofu$ go build .
go: downloading github.com/apparentlymart/go-userdirs v0.0.0-20200915174352-b0c018a67c13
go: downloading github.com/hashicorp/terraform-svchost v0.1.1
go: downloading github.com/apparentlymart/go-shquot v0.0.1
go: downloading go.opentelemetry.io/contrib/exporters/autoexport v0.0.0-20230703072336-9a582bd098a2
go: downloading github.com/mitchellh/cli v1.1.5
go: downloading github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db
go: downloading github.com/hashicorp/go-plugin v1.4.3
go: downloading github.com/mattn/go-shellwords v1.0.4
go: downloading go.opentelemetry.io/otel/sdk v1.16.0
go: downloading go.opentelemetry.io/otel v1.16.0
go: downloading go.opentelemetry.io/otel/trace v1.16.0
go: downloading github.com/zclconf/go-cty v1.13.2
go: downloading github.com/hashicorp/hcl v1.0.0
go: downloading github.com/mitchellh/go-wordwrap v1.0.1
go: downloading github.com/hashicorp/hcl/v2 v2.17.0
go: downloading github.com/hashicorp/terraform-registry-address v0.2.0
go: downloading golang.org/x/text v0.10.0
go: downloading github.com/pkg/browser v0.0.0-20201207095918-0426ae3fba23
go: downloading github.com/hashicorp/go-hclog v1.4.0
go: downloading github.com/apparentlymart/go-versions v1.0.1
go: downloading github.com/bgentry/speakeasy v0.1.0
go: downloading github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
go: downloading github.com/hashicorp/go-getter v1.7.2
go: downloading github.com/hashicorp/go-tfe v1.32.0
go: downloading github.com/hashicorp/go-uuid v1.0.3
go: downloading github.com/hashicorp/go-version v1.6.0
go: downloading github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0
go: downloading github.com/mattn/go-isatty v0.0.17
go: downloading github.com/posener/complete v1.2.3
go: downloading github.com/xlab/treeprint v0.0.0-20161029104018-1d6e34225557
go: downloading golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17
go: downloading golang.org/x/oauth2 v0.8.0
go: downloading github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f
go: downloading golang.org/x/term v0.9.0
go: downloading github.com/hashicorp/go-cleanhttp v0.5.2
go: downloading github.com/hashicorp/errwrap v1.1.0
go: downloading github.com/hashicorp/go-multierror v1.1.1
go: downloading github.com/agext/levenshtein v1.2.3
go: downloading github.com/ProtonMail/go-crypto v0.0.0-20230619160724-3fbb1f12458c
go: downloading github.com/hashicorp/go-retryablehttp v0.7.4
go: downloading golang.org/x/mod v0.10.0
go: downloading golang.org/x/net v0.11.0
go: downloading github.com/Masterminds/sprig/v3 v3.2.2
go: downloading github.com/armon/go-radix v1.0.0
go: downloading github.com/fatih/color v1.13.0
go: downloading github.com/mitchellh/go-homedir v1.1.0
go: downloading github.com/Azure/azure-sdk-for-go v59.2.0+incompatible
go: downloading github.com/Azure/go-autorest/autorest v0.11.24
go: downloading github.com/hashicorp/go-azure-helpers v0.43.0
go: downloading github.com/manicminer/hamilton v0.44.0
go: downloading github.com/tombuildsstuff/giovanni v0.15.1
go: downloading github.com/hashicorp/consul/api v1.13.0
go: downloading github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.588
go: downloading github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/sts v1.0.588
go: downloading github.com/Azure/go-autorest v14.2.0+incompatible
go: downloading github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/tag v1.0.233
go: downloading github.com/tencentyun/cos-go-sdk-v5 v0.7.29
go: downloading cloud.google.com/go/storage v1.28.1
go: downloading google.golang.org/api v0.114.0
go: downloading k8s.io/api v0.23.4
go: downloading k8s.io/apimachinery v0.23.4
go: downloading cloud.google.com/go v0.110.0
go: downloading k8s.io/client-go v0.23.4
go: downloading k8s.io/utils v0.0.0-20211116205334-6203023598ed
go: downloading github.com/aliyun/alibaba-cloud-sdk-go v1.61.1501
go: downloading github.com/aliyun/aliyun-oss-go-sdk v2.2.9+incompatible
go: downloading github.com/aliyun/aliyun-tablestore-go-sdk v4.1.2+incompatible
go: downloading github.com/jmespath/go-jmespath v0.4.0
go: downloading github.com/pkg/errors v0.9.1
go: downloading github.com/lib/pq v1.10.3
go: downloading github.com/aws/aws-sdk-go v1.44.122
go: downloading github.com/hashicorp/aws-sdk-go-base v0.7.1
go: downloading github.com/hashicorp/jsonapi v0.0.0-20210826224640-ee7dae0fb22d
go: downloading github.com/mitchellh/copystructure v1.2.0
go: downloading github.com/golang/protobuf v1.5.3
go: downloading github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d
go: downloading github.com/mitchellh/go-testing-interface v1.14.1
go: downloading github.com/oklog/run v1.0.0
go: downloading google.golang.org/grpc v1.56.1
go: downloading go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0
go: downloading go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0
go: downloading github.com/spf13/afero v1.9.3
go: downloading github.com/go-logr/logr v1.2.4
go: downloading go.opentelemetry.io/otel/metric v1.16.0
go: downloading golang.org/x/sys v0.9.0
go: downloading github.com/apparentlymart/go-textseg/v13 v13.0.0
go: downloading go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0
go: downloading github.com/google/go-cmp v0.5.9
go: downloading github.com/mitchellh/go-linereader v0.0.0-20190213213312-1b945b3263eb
go: downloading github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2
go: downloading github.com/zclconf/go-cty-yaml v1.0.3
go: downloading google.golang.org/protobuf v1.31.0
go: downloading github.com/mitchellh/reflectwalk v1.0.2
go: downloading github.com/davecgh/go-spew v1.1.1
go: downloading github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d
go: downloading github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d
go: downloading github.com/hashicorp/go-safetemp v1.0.0
go: downloading github.com/klauspost/compress v1.15.11
go: downloading github.com/ulikunitz/xz v0.5.10
go: downloading github.com/mattn/go-colorable v0.1.13
go: downloading github.com/mitchellh/mapstructure v1.5.0
go: downloading github.com/Masterminds/goutils v1.1.1
go: downloading github.com/Masterminds/semver/v3 v3.1.1
go: downloading github.com/huandu/xstrings v1.3.3
go: downloading github.com/google/uuid v1.3.0
go: downloading github.com/imdario/mergo v0.3.13
go: downloading github.com/shopspring/decimal v1.3.1
go: downloading github.com/spf13/cast v1.5.0
go: downloading golang.org/x/crypto v0.10.0
go: downloading github.com/google/go-querystring v1.1.0
go: downloading github.com/hashicorp/go-slug v0.12.0
go: downloading golang.org/x/sync v0.3.0
go: downloading golang.org/x/time v0.3.0
go: downloading github.com/Azure/go-autorest/logger v0.2.1
go: downloading github.com/Azure/go-autorest/tracing v0.6.0
go: downloading github.com/Azure/go-autorest/autorest/adal v0.9.18
go: downloading github.com/Azure/go-autorest/autorest/azure/cli v0.4.4
go: downloading github.com/manicminer/hamilton-autorest v0.2.0
go: downloading github.com/hashicorp/go-rootcerts v1.0.2
go: downloading github.com/hashicorp/serf v0.9.6
go: downloading github.com/Azure/go-autorest/autorest/validation v0.3.1
go: downloading cloud.google.com/go/compute/metadata v0.2.3
go: downloading cloud.google.com/go/iam v0.13.0
go: downloading github.com/googleapis/gax-go/v2 v2.7.1
go: downloading google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc
go: downloading cloud.google.com/go/compute v1.19.1
go: downloading github.com/mozillazg/go-httpheader v0.3.0
go: downloading github.com/gogo/protobuf v1.3.2
go: downloading k8s.io/klog/v2 v2.30.0
go: downloading github.com/google/gofuzz v1.1.0
go: downloading github.com/spf13/pflag v1.0.5
go: downloading google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc
go: downloading github.com/vmihailenco/msgpack/v5 v5.3.5
go: downloading github.com/go-logr/stdr v1.2.2
go: downloading github.com/apparentlymart/go-cidr v1.1.0
go: downloading github.com/bmatcuk/doublestar v1.1.5
go: downloading github.com/json-iterator/go v1.1.12
go: downloading github.com/modern-go/reflect2 v1.0.2
go: downloading go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0
go: downloading go.opentelemetry.io/proto/otlp v0.20.0
go: downloading github.com/cloudflare/circl v1.3.3
go: downloading github.com/Azure/go-autorest/autorest/date v0.3.0
go: downloading github.com/golang-jwt/jwt/v4 v4.4.2
go: downloading github.com/dimchansky/utfbom v1.1.1
go: downloading go.opencensus.io v0.24.0
go: downloading golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2
go: downloading google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc
go: downloading sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6
go: downloading sigs.k8s.io/structured-merge-diff/v4 v4.2.1
go: downloading gopkg.in/inf.v0 v0.9.1
go: downloading sigs.k8s.io/yaml v1.2.0
go: downloading github.com/googleapis/gnostic v0.5.5
go: downloading github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da
go: downloading github.com/xanzy/ssh-agent v0.3.1
go: downloading github.com/masterzen/winrm v0.0.0-20200615185753-c42b5136ff88
go: downloading github.com/packer-community/winrmcp v0.0.0-20180921211025-c76d91c1e7db
go: downloading gopkg.in/ini.v1 v1.66.2
go: downloading github.com/vmihailenco/tagparser/v2 v2.0.0
go: downloading github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd
go: downloading github.com/cenkalti/backoff/v4 v4.2.1
go: downloading github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0
go: downloading github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da
go: downloading github.com/googleapis/enterprise-certificate-proxy v0.2.3
go: downloading gopkg.in/yaml.v2 v2.4.0
go: downloading k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65
go: downloading github.com/hashicorp/go-immutable-radix v1.0.0
go: downloading github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c
go: downloading github.com/ChrisTrenkamp/goxpath v0.0.0-20190607011252-c5096ec8773d
go: downloading github.com/gofrs/uuid v4.0.0+incompatible
go: downloading github.com/dylanmei/iso8601 v0.1.0
go: downloading github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d
go: downloading github.com/masterzen/simplexml v0.0.0-20190410153822-31eea3082786
go: downloading github.com/hashicorp/golang-lru v0.5.1
go: downloading gopkg.in/yaml.v3 v3.0.1
go: downloading github.com/Azure/go-autorest/autorest/to v0.4.0
# go.opentelemetry.io/otel/sdk/trace
../../go/pkg/mod/go.opentelemetry.io/otel/sdk@v1.16.0/trace/provider.go:78:24: undefined: atomic.Pointer
../../go/pkg/mod/go.opentelemetry.io/otel/sdk@v1.16.0/trace/provider.go:80:20: undefined: atomic.Bool
note: module requires Go 1.19

This first build failed and I was a bit confused. I realized i didn’t source my bashrc after installing golang 1.19.

Checking, indeed I had 1.18 still active in the path, which I quickly fixed

builder@DESKTOP-QADGF36:~/Workspaces/opentofu$ go version
go version go1.18.5 linux/amd64
builder@DESKTOP-QADGF36:~/Workspaces/opentofu$ export PATH="/home/linuxbrew/.linuxbrew/opt/go@1.19/bin:$PATH"
builder@DESKTOP-QADGF36:~/Workspaces/opentofu$ go version
go version go1.19.13 linux/amd64

Now when I build, I see no errors and a new opentofu binary exists

/content/images/2023/09/opentofu-01.png

We can check the version

builder@DESKTOP-QADGF36:~/Workspaces/opentofu$ ./opentofu version
OpenTofu v1.6.0-dev
on linux_amd64

I’ll copy to /usr/local/bin to make it easy to use

builder@DESKTOP-QADGF36:~/Workspaces$ sudo cp opentofu/opentofu /usr/local/bin/
[sudo] password for builder:

Testing

Last week, we covered using GCP Infra Manager and there I used the example repo https://github.com/idjohnson/gcpimexample.

I’ll clone that down

builder@DESKTOP-QADGF36:~/Workspaces$ git clone https://github.com/idjohnson/gcpimexample
Cloning into 'gcpimexample'...
remote: Enumerating objects: 19, done.
remote: Counting objects: 100% (19/19), done.
remote: Compressing objects: 100% (19/19), done.
remote: Total 19 (delta 8), reused 0 (delta 0), pack-reused 0
Unpacking objects: 100% (19/19), 7.81 KiB | 2.60 MiB/s, done.

Our first step with T… Tofu is to ‘init’

builder@DESKTOP-QADGF36:~/Workspaces/gcpimexample$ opentofu init

Initializing the backend...

Initializing provider plugins...
- Finding latest version of hashicorp/google...
- Installing hashicorp/google v4.83.0...
- Installed hashicorp/google v4.83.0 (signed, key ID 34365D9472D7468F)

Providers are signed by their developers.
If you'd like to know more about provider signing, you can read about it here:
https://www.placeholderplaceholderplaceholder.io/docs/cli/plugins/signing.html

OpenTofu has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that OpenTofu can guarantee to make the same selections by default when
you run "tofu init" in the future.

OpenTofu has been successfully initialized!

You may now begin working with OpenTofu. Try running "tofu plan" to see
any changes that are required for your infrastructure. All OpenTofu commands
should now work.

If you ever set or change modules or backend configuration for OpenTofu,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.

With no values passed or set explicitly in my values file, running a plan will prompt me for the bucket and project

builder@DESKTOP-QADGF36:~/Workspaces/gcpimexample$ opentofu plan
var.bucketname
  The name of the bucket to try and create

  Enter a value: idjtestbucket33445566

var.projectid
  The name of the GCP project

  Enter a value: myanthosproject2


Planning failed. OpenTofu encountered an error while generating this plan.

╷
│ Error: Invalid provider configuration
│
│ Provider "registry.terraform.io/hashicorp/google" requires explicit configuration. Add a provider block to the root
│ module and configure the provider's required arguments as described in the provider documentation.
│
╵
╷
│ Error: Attempted to load application default credentials since neither `credentials` nor `access_token` was set in the provider block.  No credentials loaded. To use your gcloud credentials, run 'gcloud auth application-default login'
│
│   with provider["registry.terraform.io/hashicorp/google"],
│   on <empty> line 0:
│   (source code not available)
│
│ google: could not find default credentials. See https://cloud.google.com/docs/authentication/external/set-up-adc for
│ more information
╵

I’ll next run gcloud auth application-default login which fires up Firefox in X

/content/images/2023/09/opentofu-02.png

This required user, login and 2FA which was nice to see all the security controls work

/content/images/2023/09/opentofu-03.png

This time the plan worked just dandy

builder@DESKTOP-QADGF36:~/Workspaces/gcpimexample$ opentofu plan
var.bucketname
  The name of the bucket to try and create

  Enter a value: freshbrewedtest33445566

var.projectid
  The name of the GCP project

  Enter a value: myanthosproject2


OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

OpenTofu will perform the following actions:

  # google_storage_bucket.auto-expire will be created
  + resource "google_storage_bucket" "auto-expire" {
      + force_destroy               = true
      + id                          = (known after apply)
      + labels                      = (known after apply)
      + location                    = "US"
      + name                        = "freshbrewedtest33445566"
      + project                     = "myanthosproject2"
      + public_access_prevention    = (known after apply)
      + self_link                   = (known after apply)
      + storage_class               = "STANDARD"
      + uniform_bucket_level_access = (known after apply)
      + url                         = (known after apply)

      + lifecycle_rule {
          + action {
              + type = "Delete"
            }
          + condition {
              + age                   = 3
              + matches_prefix        = []
              + matches_storage_class = []
              + matches_suffix        = []
              + with_state            = (known after apply)
            }
        }
      + lifecycle_rule {
          + action {
              + type = "AbortIncompleteMultipartUpload"
            }
          + condition {
              + age                   = 1
              + matches_prefix        = []
              + matches_storage_class = []
              + matches_suffix        = []
              + with_state            = (known after apply)
            }
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Note: You didn't use the -out option to save this plan, so OpenTofu can't guarantee to take exactly these actions if
you run "tofu apply" now.

Since I neglected to save the plan, if I try and apply, it will just ask me the same questions again. Let’s save the plan this time

builder@DESKTOP-QADGF36:~/Workspaces/gcpimexample$ opentofu plan -out tofu.plan
var.bucketname
  The name of the bucket to try and create

  Enter a value: freshbrewedtest33445566

var.projectid
  The name of the GCP project

  Enter a value: myanthosproject2


OpenTofu used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

OpenTofu will perform the following actions:

  # google_storage_bucket.auto-expire will be created
  + resource "google_storage_bucket" "auto-expire" {
      + force_destroy               = true
      + id                          = (known after apply)
      + labels                      = (known after apply)
      + location                    = "US"
      + name                        = "freshbrewedtest33445566"
      + project                     = "myanthosproject2"
      + public_access_prevention    = (known after apply)
      + self_link                   = (known after apply)
      + storage_class               = "STANDARD"
      + uniform_bucket_level_access = (known after apply)
      + url                         = (known after apply)

      + lifecycle_rule {
          + action {
              + type = "Delete"
            }
          + condition {
              + age                   = 3
              + matches_prefix        = []
              + matches_storage_class = []
              + matches_suffix        = []
              + with_state            = (known after apply)
            }
        }
      + lifecycle_rule {
          + action {
              + type = "AbortIncompleteMultipartUpload"
            }
          + condition {
              + age                   = 1
              + matches_prefix        = []
              + matches_storage_class = []
              + matches_suffix        = []
              + with_state            = (known after apply)
            }
        }
    }

Plan: 1 to add, 0 to change, 0 to destroy.

──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────

Saved the plan to: tofu.plan

To perform exactly these actions, run the following command to apply:
    tofu apply "tofu.plan"

I can now apply it without issue:

builder@DESKTOP-QADGF36:~/Workspaces/gcpimexample$ opentofu apply tofu.plan
google_storage_bucket.auto-expire: Creating...
google_storage_bucket.auto-expire: Creation complete after 1s [id=freshbrewedtest33445566]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

Just as before, we can go to the cloud console to see our bucket was indeed created as well as the lifecycle policies

/content/images/2023/09/opentofu-04.png

Note: a consequence of not using remote state management is that my tfstate file is sitting locally and I would need to protect and/or manage in some fashion

builder@DESKTOP-QADGF36:~/Workspaces/gcpimexample$ ls -ltra
total 52
drwxr-xr-x 106 builder builder  4096 Sep 24 16:43 ..
-rw-r--r--   1 builder builder   204 Sep 24 16:43 variables.tf
-rw-r--r--   1 builder builder   388 Sep 24 16:43 buckets.tf
-rw-r--r--   1 builder builder    51 Sep 24 16:43 README.md
-rw-r--r--   1 builder builder 11357 Sep 24 16:43 LICENSE
drwxr-xr-x   8 builder builder  4096 Sep 24 16:43 .git
drwxr-xr-x   3 builder builder  4096 Sep 24 16:44 .terraform
-rw-r--r--   1 builder builder  1150 Sep 24 16:44 .terraform.lock.hcl
-rw-r--r--   1 builder builder  3042 Sep 24 16:50 tofu.plan
-rw-r--r--   1 builder builder  3179 Sep 24 16:50 terraform.tfstate
drwxr-xr-x   4 builder builder  4096 Sep 24 16:50 .

Cleanup

Let’s now say we are done with this bucket.

I was a bit bummed it didn’t just pick up my vars from the state file, but ah well.

Since opentofu destroy is just a shorthand of opentofu apply -destroy, i used the latter.

builder@DESKTOP-QADGF36:~/Workspaces/gcpimexample$ terraform apply -destroy -var bucketname=freshbrewedtest33445566 -va
r projectid=myanthosproject2
google_storage_bucket.auto-expire: Refreshing state... [id=freshbrewedtest33445566]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with
the following symbols:
  - destroy

Terraform will perform the following actions:

  # google_storage_bucket.auto-expire will be destroyed
  - resource "google_storage_bucket" "auto-expire" {
      - default_event_based_hold    = false -> null
      - force_destroy               = true -> null
      - id                          = "freshbrewedtest33445566" -> null
      - labels                      = {} -> null
      - location                    = "US" -> null
      - name                        = "freshbrewedtest33445566" -> null
      - project                     = "myanthosproject2" -> null
      - public_access_prevention    = "inherited" -> null
      - requester_pays              = false -> null
      - self_link                   = "https://www.googleapis.com/storage/v1/b/freshbrewedtest33445566" -> null
      - storage_class               = "STANDARD" -> null
      - uniform_bucket_level_access = false -> null
      - url                         = "gs://freshbrewedtest33445566" -> null

      - lifecycle_rule {
          - action {
              - type = "Delete" -> null
            }
          - condition {
              - age                        = 3 -> null
              - days_since_custom_time     = 0 -> null
              - days_since_noncurrent_time = 0 -> null
              - matches_prefix             = [] -> null
              - matches_storage_class      = [] -> null
              - matches_suffix             = [] -> null
              - num_newer_versions         = 0 -> null
              - with_state                 = "ANY" -> null
            }
        }
      - lifecycle_rule {
          - action {
              - type = "AbortIncompleteMultipartUpload" -> null
            }
          - condition {
              - age                        = 1 -> null
              - days_since_custom_time     = 0 -> null
              - days_since_noncurrent_time = 0 -> null
              - matches_prefix             = [] -> null
              - matches_storage_class      = [] -> null
              - matches_suffix             = [] -> null
              - num_newer_versions         = 0 -> null
              - with_state                 = "ANY" -> null
            }
        }
    }

Plan: 0 to add, 0 to change, 1 to destroy.

Do you really want to destroy all resources?
  Terraform will destroy all your managed infrastructure, as shown above.
  There is no undo. Only 'yes' will be accepted to confirm.

  Enter a value: yes

google_storage_bucket.auto-expire: Destroying... [id=freshbrewedtest33445566]
google_storage_bucket.auto-expire: Destruction complete after 1s

Apply complete! Resources: 0 added, 0 changed, 1 destroyed.

And as before, I can see the bucket is removed

/content/images/2023/09/opentofu-05.png

And just to be certain that despite having to pass in values via “-var”, it was really operating against just that which it had created, I went ahead and tried to use the same local terraform to destroy a bucket not created by this plan/apply.

/content/images/2023/09/opentofu-06.png

That just shows that, indeed, it is operating against tfstate file

Summary

Today we built OpenTofu locally:

builder@DESKTOP-QADGF36:~/Workspaces/gcpimexample$ opentofu version
OpenTofu v1.6.0-dev
on linux_amd64
+ provider registry.terraform.io/hashicorp/google v4.83.0

Using GoLang 1.19 and the Github repo. We tested a simple plane we used with GCP earlier this week. We confirmed we can create and destroy with ease.

Next, we’ll look at using this in an automated pipeline, even building a container image to reduce the needs for recompiles.

What I believe - Hashi and the BUSL Change

When Hashi moved all of their licensed products to the BSL 1.1 on August 10th it was a smack in the face to all the Open Source work that has gone out there. That August 10th blog post attempted to align the BUSL change in with the type that, say, MongoDB did:

BSL 1.1 is a source-available license that allows copying, modification, redistribution, non-commercial use, and commercial use under specific conditions. With this change we are following a path similar to other companies in recent years. These companies include Couchbase, Cockroach Labs, Sentry, and MariaDB, which developed this license in 2013. Companies including Confluent, MongoDB, Elastic, Redis Labs, and others have also adopted alternative licenses that include restrictions on commercial usage. In all these cases, the license enables the commercial sponsor to have more control around commercialization.

But that isn’t quite accurate. We can see from the details of the SSPL it’s key point is a poison pill for those that would try to make cloud copies in that they would be required to opensource everything if they did - like a mini-GPL bomb. Frankly, I like that approach. MariaDB is all in on GPL v2 with the exception of their Enterprise product, for which they used the BSL just for MaxScale 2.0.0 to 2.0.4 : BSL 1.1 for these projects (linked to MaxScale 2). For MariaDB MaxScale 2.0.0 until 2.0.4, BSL 1.0 . MariaDB Server will continue to be licensed under GPL in perpetuity, while its connectors will continue to be under the LGPL.

Armon went on to write:

Our implementation of BSL includes additional usage grants that allow for broadly permissive use of our source code. We believe this will offer a fair and sustainable way for HashiCorp to share its source code widely, for free use. We consulted with OSS licensing experts and other industry stakeholders when developing our license, so that our efforts would be in line with industry practices.

Our first goal with this change is to minimize the impact to our community, partners, and customers. We will continue to publish source code and updates for HashiCorp products to our GitHub repository and distribution channels.

End users can continue to copy, modify, and redistribute the code for all non-commercial and commercial use, except where providing a competitive offering to HashiCorp. Partners can continue to build integrations for our joint customers. We will continue to work closely with the cloud service providers to ensure deep support for our mutual technologies. Customers of enterprise and cloud-managed HashiCorp products will see no change as well.

This is the part that has folks up in arms: “except where providing a competitive offering to HashiCorp”. Use the source code, some of which you might have written, provided you don’t compete with us. And as time goes forward, Hashicorp covers progressively more and more ground.

What if someone like Harness.io put in Terraform changes but then was sued for using Terraform code in their product because now Hashi has Waypoint, a pipelining app? And we can see from the Hashicorp hosted discussion that followed, many had the same question:

/content/images/2023/09/opentofu-07.png

In this long thread, Flux almost dropped Terraform and spoke of switching away from Vault API usage because, according to the CNCF, BUSL is not Open-Source and is not allowed.

Look, I do not begrudge anyone for wanting to protect their IP. We’ve seen what the cloud providers do to smaller companies. I’ve been in the room where we didn’t opt for Redis (Enterprise) because we could just get the equivalent from GCP or Amazon; which was ripped off in it’s entirety from Redis (corp).

But where the BUSL model really gets rough is with Terraform.

Terraform, to be specific, is only as big as it is, and as adopted as it is, because of the contributions of others. If GCP stops updating their TF modules, I stop using Terraform. I don’t use Terraform to, I dunno, enjoy playing with Opa and Plans - I use it fundamentally to build cloud infrastructure.

Consider, if you will, what would happen if Apple decided too many people made money off Safari so the Safari browser would only surf Apple owned products or fully free websites - would you still use Safari? What if it was illegal to use a MacBook to build a website that competed with an Apple product, would you consider buying a new one - or more importantly, releasing your software to run on said MacBook?

That’s where I really challenge the “Customers of enterprise and cloud-managed HashiCorp products will see no change as well” line; We have seen change already with the creation of OpenTofu with large backing - the Linux Foundation is behind this - even the CNCF might be adopting it - the folks that keep Kubernetes marching forward. And only a few weeks after the announcement, Google unveiled the Infra Manager that aims to obliviate the need for Terraform Enterprise/Cloud when standing up infrastructure in GCP.

Google’s offering is not as good as TFE/TF Cloud right now, let’s be real, but I now have a non-Hashi solution to handling GCP infrastructure. With some creative pipelining in Github Workflows or AzDO I could bring it close to equivocating TFE or at least TF Cloud.

Pulumi, perhaps the little train that could, a company that made me chuckle by hanging out in the lobby at the last Hashiconf handing out fliers (and putting signs on all the stoplights around the venue), threw a bit of shade pointing out they are Apache 2.0 and plan to stay that way

/content/images/2023/09/opentofu-11.png

Frankly, I think Hashi is close to losing the battle here unless they do an immediate about-face on the BUSL. I believe Terraform, in the form of OpenTofu will keep going - and be fully supported by the cloud providers. I think modules from Amazon, Google, Microsoft and more will pin to OpenTF not to HashiTF.

I saw all this before - I saw the world swing from Hudson to Jenkins, from MySQL to MariaDB, from Java to OpenJDK and C#. How many companies have you been to, as a software person, still using Oracle Java? using Oracle Hudson? Using MySQL Enterprise? Does the enterprise customer want to pin to an offering from one provider who has changed the pricing several times already?

In that forum, I saw several questions around the fear of a Vader move (I am altering the deal…)

/content/images/2023/09/opentofu-08.png

Another wrote:

/content/images/2023/09/opentofu-09.png

One can get defensive and just say this is all just a bunch of Open Source people griping, but the stock has taken a hit since

/content/images/2023/09/opentofu-10.png

I need to pause here and say I have drafted this post for a bit and gotten feedback from several trusted architects and colleagues. I have wrestled internally with what to say.

I will simply say that the License change truely disappointed me. I can add that this is likely my last Hashiconf as the love has dissipated. I’ve cashed in my chits every year with every employer to go to Hashiconf since 2018, but I’ll go somewhere else next year, maybe Kubecon. I’ve been so all in for years.

/content/images/2023/09/opentofu-12.png image: Me with the CEO

I have spoken on their behalf to large rooms, as recently as two weeks ago (in partnership with a vendor, Datadog, of which I’m still all-in).

/content/images/2023/09/opentofu-13.png

image: Me in Sept speaking on behalf of Datadog and Hashicorp with the field CTO

I have been their steadfast advocate trying to land Vault, Consul and Terraform Enterprise at a variety of companies. I believe I have been a really valuable ally for what was then, a smaller open-source company looking to grow. But I just cannot anymore.

Don’t get me wrong, I still think TFE, as of this point in time right now, is the strongest IaC Enterprise play. But now I would now advise caution in its adoption. I might propose alternatives. I might now invite others into the room of architects and push for pilots of Pulumi, GCP Infra or just doing pipelines on our own.

Maybe I’m just one guy. But I’ll stay steadfast in my belief that Open-Source (not source available) is the strongest play always. And when companies start Open Source then break that trust, as Oracle did with Java and Hudson, or Atlassian (technically Bob Swift) did with the JIRA CLI, then those strong (often senior in organizations) allies stop being allies. It’s a very hard trust to win back.

OpenTofu Terraform Hashicorp

Have something to add? Feedback? Try our new forums

Isaac Johnson

Isaac Johnson

Cloud Solutions Architect

Isaac is a CSA and DevOps engineer who focuses on cloud migrations and devops processes. He also is a dad to three wonderful daughters (hence the references to Princess King sprinkled throughout the blog).

Theme built by C.S. Rhymes