Skip to content

Commit ca032ba

Browse files
committed
Add docs and examples
Signed-off-by: Fatih Türken <turkenf@gmail.com>
1 parent b737818 commit ca032ba

9 files changed

Lines changed: 695 additions & 0 deletions
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
# Remote Module Pull Policy
2+
3+
This guide explains how to use the `remotePullPolicy` feature to control when provider-opentofu downloads remote OpenTofu modules, significantly reducing network costs and improving reconciliation performance.
4+
5+
## Overview
6+
7+
By default, provider-opentofu downloads remote OpenTofu modules on every reconciliation. For workspaces that reconcile frequently (every 1-10 minutes), this can result in substantial network egress costs.
8+
9+
**Example scenario** (50MB module, 10-minute poll interval):
10+
- **Without pull policy control**: 50MB module × 6 reconciliations/hour × 24 hours = 7.2GB/day per workspace
11+
- **With IfNotPresent policy**: 50MB (single download) = 50MB/day per workspace
12+
- **Network reduction**: 98.6% savings in this scenario
13+
14+
*Actual savings depend on your module size and reconciliation frequency.*
15+
16+
The `remotePullPolicy` field gives you control over when modules are downloaded, allowing you to optimize for either cost or freshness.
17+
18+
## Pull Policy Options
19+
20+
### Always (Default)
21+
22+
Downloads the remote module on every reconciliation.
23+
24+
**Use cases:**
25+
- Development workspaces where you need the latest module changes
26+
- Modules without pinned versions (e.g., `ref=main` instead of `ref=v1.0.0`)
27+
- When module content changes frequently without version updates
28+
29+
**Example:**
30+
```yaml
31+
apiVersion: opentofu.upbound.io/v1beta1
32+
kind: Workspace
33+
metadata:
34+
name: example-always
35+
spec:
36+
forProvider:
37+
source: Remote
38+
module: git::https://github.com/org/repo?ref=main
39+
remotePullPolicy: Always # Explicit (default behavior)
40+
```
41+
42+
**Network impact:**
43+
- Downloads on every reconciliation
44+
- Highest network costs
45+
- Ensures latest module content
46+
47+
### IfNotPresent
48+
49+
Downloads the remote module only once, reusing it on subsequent reconciliations until the module URL changes.
50+
51+
**Use cases:**
52+
- Production workspaces with pinned module versions
53+
- Cost-sensitive environments
54+
- Large modules (>10MB) that are expensive to download repeatedly
55+
- High reconciliation frequency (every 1-5 minutes)
56+
57+
**Example:**
58+
```yaml
59+
apiVersion: opentofu.upbound.io/v1beta1
60+
kind: Workspace
61+
metadata:
62+
name: example-if-not-present
63+
spec:
64+
forProvider:
65+
source: Remote
66+
module: git::https://github.com/org/repo?ref=v1.0.0 # Pinned version
67+
remotePullPolicy: IfNotPresent
68+
```
69+
70+
**Network impact:**
71+
- Downloads once on first reconciliation
72+
- Significant network reduction (up to 98%+ depending on module size and poll interval)
73+
- Faster reconciliation (no download time)
74+
75+
**Automatic re-download triggers:**
76+
- Module URL changes (including git ref)
77+
- Workspace pod restart (without persistent volume)
78+
- `.terraform.lock.hcl` file is deleted or missing
79+
- Interrupted `tofu init` (lock file not created)
80+
81+
## How It Works
82+
83+
### Detection Mechanism
84+
85+
The `IfNotPresent` policy checks for the presence of a valid `.terraform.lock.hcl` file in the workspace. This file is created at the END of a successful `tofu init`, making it the most reliable indicator that the module has been successfully initialized. When an `entrypoint` is specified, the check is performed in the entrypoint subdirectory (where tofu actually runs), not the base workspace directory.
86+
87+
**Why .terraform.lock.hcl?**
88+
- Created only after successful `tofu init` completion
89+
- Interrupted `tofu init` leaves `.terraform/` directory but NO lock file
90+
- Located at workspace root (no entrypoint path confusion)
91+
- Contains provider dependency information
92+
93+
**Detection logic:**
94+
```
95+
workspace_dir = base_workspace_directory
96+
if entrypoint is specified:
97+
workspace_dir = base_workspace_directory/entrypoint
98+
99+
if .terraform.lock.hcl exists and is valid in workspace_dir:
100+
if module URL == previously downloaded URL:
101+
→ Skip download (reuse existing module)
102+
else:
103+
→ Download (module URL changed)
104+
else:
105+
→ Download (module not initialized)
106+
```
107+
108+
**Example with entrypoint:**
109+
```yaml
110+
spec:
111+
forProvider:
112+
source: Remote
113+
module: git::https://github.com/terraform-aws-modules/terraform-aws-vpc.git?ref=v5.1.2
114+
entrypoint: examples/complete
115+
remotePullPolicy: IfNotPresent
116+
```
117+
118+
In this case:
119+
- Module is downloaded to: `/tofu/workspace-uid/`
120+
- Tofu runs in: `/tofu/workspace-uid/examples/complete/`
121+
- `.terraform.lock.hcl` check looks in: `/tofu/workspace-uid/examples/complete/.terraform.lock.hcl`
122+
123+
### Status Tracking
124+
125+
The provider tracks the last downloaded module URL in the workspace status:
126+
127+
```yaml
128+
status:
129+
atProvider:
130+
remoteSource: git::https://github.com/org/repo?ref=v1.0.0
131+
```
132+
133+
This allows automatic detection of module URL changes, triggering a re-download when needed.
134+
135+
## Migration Guide
136+
137+
### Migrating Existing Workspaces
138+
139+
Existing workspaces without `remotePullPolicy` will continue using the default `Always` behavior. To opt-in to cost savings:
140+
141+
1. **Ensure your module versions are pinned:**
142+
```yaml
143+
# Good - pinned version
144+
module: git::https://github.com/org/repo?ref=v1.0.0
145+
146+
# Avoid - floating ref
147+
module: git::https://github.com/org/repo?ref=main
148+
```
149+
150+
2. **Add the remotePullPolicy field:**
151+
```yaml
152+
spec:
153+
forProvider:
154+
source: Remote
155+
module: git::https://github.com/org/repo?ref=v1.0.0
156+
remotePullPolicy: IfNotPresent # Add this line
157+
```
158+
159+
3. **Apply the change:**
160+
```bash
161+
kubectl apply -f workspace.yaml
162+
```
163+
164+
The first reconciliation after the change will download the module normally. Subsequent reconciliations will skip the download.
165+
166+
### Updating Module Versions
167+
168+
When you need to update to a new module version:
169+
170+
1. **Update the module reference:**
171+
```yaml
172+
spec:
173+
forProvider:
174+
module: git::https://github.com/org/repo?ref=v2.0.0 # Changed from v1.0.0
175+
remotePullPolicy: IfNotPresent
176+
```
177+
178+
2. **Apply the change:**
179+
```bash
180+
kubectl apply -f workspace.yaml
181+
```
182+
183+
The provider will automatically detect the URL change and download the new version.
184+
185+
## Best Practices
186+
187+
### 1. Pin Module Versions in Production
188+
189+
Always use specific version tags or commit SHAs in production:
190+
191+
```yaml
192+
# Recommended - specific version tag
193+
module: git::https://github.com/terraform-aws-modules/terraform-aws-vpc?ref=v5.1.2
194+
195+
# Recommended - specific commit
196+
module: git::https://github.com/org/repo?ref=abc123def
197+
198+
# Avoid - floating ref
199+
module: git::https://github.com/org/repo?ref=main
200+
```
201+
202+
### 2. Use IfNotPresent with Pinned Versions
203+
204+
Combine version pinning with IfNotPresent for maximum cost savings:
205+
206+
```yaml
207+
spec:
208+
forProvider:
209+
source: Remote
210+
module: git::https://github.com/org/repo?ref=v1.0.0
211+
remotePullPolicy: IfNotPresent
212+
```
213+
214+
### 3. Use Always for Development
215+
216+
Keep the Always policy for development workspaces where you need the latest changes:
217+
218+
```yaml
219+
spec:
220+
forProvider:
221+
source: Remote
222+
module: git::https://github.com/org/repo?ref=develop
223+
remotePullPolicy: Always # Get latest changes
224+
```
225+
226+
### 4. Monitor Network Costs
227+
228+
After enabling IfNotPresent, monitor your cloud provider's network egress metrics to verify cost savings:
229+
230+
```bash
231+
# Example: Check workspace reconciliation logs
232+
kubectl logs -n upbound-system deploy/provider-opentofu-* | grep "Remote module"
233+
234+
# Expected with IfNotPresent:
235+
# First reconciliation: "Remote module downloaded"
236+
# Subsequent reconciliations: "Remote module already present, skipping download"
237+
```
238+
239+
## Troubleshooting
240+
241+
### Module Not Updating After URL Change
242+
243+
**Symptoms:**
244+
- Module URL changed but old module content is still being used
245+
- Status field shows old URL
246+
247+
**Solution:**
248+
Check that the workspace reconciliation completed successfully:
249+
250+
```bash
251+
kubectl describe workspace <name>
252+
kubectl logs -n upbound-system deploy/provider-opentofu-* | grep "Remote module URL changed"
253+
```
254+
255+
### Module Downloaded Every Time Despite IfNotPresent
256+
257+
**Possible causes:**
258+
259+
1. **Workspace pods are restarting frequently**
260+
- Module is downloaded to pod's ephemeral storage
261+
- Pod restart = module is lost
262+
- Solution: Use persistent volumes for `/tofu` directory (future enhancement)
263+
264+
2. **.terraform.lock.hcl file is being deleted**
265+
- Check if any process is cleaning up the workspace directory
266+
- Verify workspace directory permissions
267+
- Check if `tofu init` is completing successfully (lock file created at END)
268+
269+
3. **Module URL is changing on every reconciliation**
270+
- Check if dynamic refs are being used
271+
- Verify status.atProvider.remoteSource matches spec.forProvider.module
272+
273+
### Status Field Not Populated
274+
275+
**Symptoms:**
276+
- `status.atProvider.remoteSource` is empty
277+
- Module downloads every time even with IfNotPresent
278+
279+
**Solution:**
280+
Wait for one successful reconciliation. The status field is populated after the first download:
281+
282+
```bash
283+
kubectl get workspace <name> -o jsonpath='{.status.atProvider.remoteSource}'
284+
```
285+
286+
## Cost Analysis
287+
288+
### Network Savings Example
289+
290+
**Scenario:**
291+
- 100 workspaces
292+
- 50MB module size
293+
- 6 reconciliations per hour
294+
- $0.12/GB network egress (AWS example)
295+
296+
**Without IfNotPresent:**
297+
- Per workspace: 50MB × 6 × 24 = 7.2GB/day
298+
- 100 workspaces: 720GB/day
299+
- Monthly cost: 720GB × 30 × $0.12 = $2,592/month
300+
301+
**With IfNotPresent:**
302+
- Per workspace: 50MB/day (one download)
303+
- 100 workspaces: 5GB/day
304+
- Monthly cost: 5GB × 30 × $0.12 = $18/month
305+
306+
**Savings: $2,574/month (99.3% reduction)**
307+
308+
### Performance Improvement
309+
310+
**Download time savings:**
311+
- 50MB module at 100Mbps = 4 seconds download time
312+
- 6 reconciliations/hour × 4 seconds = 24 seconds/hour wasted
313+
- With IfNotPresent: 4 seconds once, then <1 second for subsequent reconciliations
314+
- **Reconciliation speedup: 75%+ after first reconciliation**
315+
316+
## Limitations
317+
318+
### Current Limitations
319+
320+
1. **No cross-workspace deduplication**
321+
- Each workspace downloads its own copy of the module
322+
- 100 workspaces using the same module = 100 downloads (one per workspace)
323+
- Mitigated by: Each workspace only downloads once with IfNotPresent
324+
325+
2. **Module lost on pod restart**
326+
- Modules are stored in pod's ephemeral storage
327+
- Pod restart requires re-download
328+
- Mitigation: Use persistent volumes (manual setup)
329+
330+
3. **No content-based detection**
331+
- Detection is based on URL comparison, not module content hash
332+
- Changing module content without changing URL is not detected
333+
- Best practice: Always update version tags when changing modules
334+
335+
### Future Enhancements
336+
337+
Potential improvements under consideration:
338+
339+
1. **Provider-level cache**: Share modules across all workspaces
340+
2. **Content hashing**: Detect module changes without URL changes
341+
3. **Persistent storage**: Recommend PVC for `/tofu` directory
342+
4. **Cache warming**: Pre-download popular modules
343+
344+
## Compatible with All Module Sources
345+
346+
The `remotePullPolicy` field is supported for:
347+
- Remote sources (git, http, S3, etc.)
348+
- Not applicable to Inline sources (module is already in the spec)
349+
350+
## Summary
351+
352+
- Use `remotePullPolicy: IfNotPresent` with pinned module versions for 98%+ network cost savings
353+
- Use `remotePullPolicy: Always` (default) for development or when module freshness is critical
354+
- The provider automatically re-downloads modules when URLs change
355+
- Monitor logs and status fields to verify expected behavior
356+
- Combine with persistent volumes for maximum module reuse across pod restarts

0 commit comments

Comments
 (0)