Integrating shell scripts with Terraform for enhanced automation capabilities while maintaining Infrastructure as Code benefits.
Local-Exec Provisioner
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37 | # main.tf - Local execution on Terraform host
resource "aws_instance" "web" {
ami = "ami-0c02fb55956c7d316"
instance_type = "t3.micro"
tags = {
Name = "WebServer"
}
# Execute shell script locally after resource creation
provisioner "local-exec" {
command = <<EOT
#!/bin/bash
set -e
echo "Configuring local environment for new instance..."
# Wait for SSH to be available
until ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no ubuntu@${self.public_ip} 'echo ready'; do
echo "Waiting for SSH..."
sleep 10
done
# Upload configuration files
scp -o StrictHostKeyChecking=no config/web.conf ubuntu@${self.public_ip}:/tmp/
# Run remote configuration
ssh -o StrictHostKeyChecking=no ubuntu@${self.public_ip} << 'REMOTE_EOF'
sudo mv /tmp/web.conf /etc/nginx/sites-available/default
sudo systemctl restart nginx
sudo systemctl status nginx
REMOTE_EOF
echo "Instance configuration completed"
EOT
}
}
|
Remote-Exec Provisioner
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51 | # main.tf - Remote execution on created resources
resource "aws_instance" "app" {
ami = "ami-0c02fb55956c7d316"
instance_type = "t3.medium"
# Remote execution on the instance
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y docker.io",
"sudo systemctl start docker",
"sudo systemctl enable docker",
"sudo usermod -aG docker ubuntu"
]
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
}
# File provisioner to upload shell scripts
provisioner "file" {
source = "scripts/setup-app.sh"
destination = "/tmp/setup-app.sh"
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
}
# Execute uploaded script
provisioner "remote-exec" {
inline = [
"chmod +x /tmp/setup-app.sh",
"sudo /tmp/setup-app.sh"
]
connection {
type = "ssh"
user = "ubuntu"
private_key = file("~/.ssh/id_rsa")
host = self.public_ip
}
}
}
|
🛠️ Advanced Shell Integration Patterns
Dynamic Configuration with External Data
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29 | # data.tf - External data sources with shell scripts
data "external" "system_info" {
program = ["bash", "-c", <<EOT
#!/bin/bash
set -e
# Gather system information
echo "{"
echo " \"hostname\": \"$(hostname)\","
echo " \"os\": \"$(uname -s)\","
echo " \"arch\": \"$(uname -m)\","
echo " \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\","
echo " \"user\": \"$(whoami)\""
echo "}"
EOT
]
}
# Use external data in resources
resource "aws_instance" "dynamic" {
ami = "ami-0c02fb55956c7d316"
instance_type = "t3.micro"
tags = {
Name = "DynamicInstance-${data.external.system_info.result.hostname}"
CreatedBy = data.external.system_info.result.user
CreatedAt = data.external.system_info.result.timestamp
}
}
|
Conditional Provisioning
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 | # conditional.tf - Conditional shell execution
variable "environment" {
type = string
default = "development"
}
resource "aws_instance" "conditional" {
ami = "ami-0c02fb55956c7d316"
instance_type = "t3.micro"
# Conditional provisioning based on environment
provisioner "local-exec" {
when = create
command = <<EOT
#!/bin/bash
set -e
case "${var.environment}" in
production)
echo "Running production setup..."
# Production-specific configuration
./scripts/production-setup.sh
;;
staging)
echo "Running staging setup..."
# Staging-specific configuration
./scripts/staging-setup.sh
;;
development)
echo "Running development setup..."
# Development-specific configuration
./scripts/development-setup.sh
;;
*)
echo "Unknown environment: ${var.environment}" >&2
exit 1
;;
esac
EOT
}
}
|
Error Handling and Retry Logic
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 | # resilient.tf - Robust shell execution with error handling
resource "null_resource" "resilient_provision" {
triggers = {
instance_id = aws_instance.web.id
}
provisioner "local-exec" {
command = <<EOT
#!/bin/bash
set -e
# Retry logic with exponential backoff
retry_command() {
local cmd="$1"
local max_attempts=${2:-5}
local attempt=1
while [ $attempt -le $max_attempts ]; do
echo "Attempt $attempt/$max_attempts: $cmd"
if eval "$cmd"; then
echo "Command succeeded"
return 0
else
local exit_code=$?
echo "Command failed with exit code $exit_code"
if [ $attempt -eq $max_attempts ]; then
echo "Max attempts reached, giving up"
return $exit_code
fi
# Exponential backoff
local delay=$((2 ** attempt))
echo "Waiting $delay seconds before retry..."
sleep $delay
attempt=$((attempt + 1))
fi
done
}
# Retry critical operations
retry_command "aws ec2 wait instance-status-ok --instance-ids ${aws_instance.web.id}"
retry_command "ssh -o ConnectTimeout=10 -o StrictHostKeyChecking=no ubuntu@${aws_instance.web.public_ip} 'echo ready'"
# Main provisioning logic
echo "Starting provisioning..."
ssh -o StrictHostKeyChecking=no ubuntu@${aws_instance.web.public_ip} << 'REMOTE_EOF'
# Install and configure application
sudo apt-get update
sudo apt-get install -y nginx
sudo systemctl start nginx
sudo systemctl enable nginx
# Verify installation
if systemctl is-active --quiet nginx; then
echo "Nginx installed and running successfully"
else
echo "Nginx installation failed" >&2
exit 1
fi
REMOTE_EOF
echo "Provisioning completed successfully"
EOT
}
}
|
Secure Script Handling
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 | # secure-scripts.tf - Secure shell script management
resource "local_file" "secure_script" {
content = <<EOT
#!/bin/bash
# secure-deployment.sh - Secure deployment script
set -euo pipefail
# Use secure temporary files
TEMP_DIR=$(mktemp -d) || {
echo "Failed to create temporary directory" >&2
exit 1
}
# Clean up on exit
trap 'rm -rf "$TEMP_DIR"' EXIT
# Validate inputs
validate_required() {
local var_name="$1"
local var_value="${!var_name:-}"
if [ -z "$var_value" ]; then
echo "Error: Required variable $var_name is not set" >&2
return 1
fi
}
# Secure credential handling
if [ -f "/run/secrets/deployment-key" ]; then
DEPLOYMENT_KEY=$(cat /run/secrets/deployment-key)
else
echo "Error: Deployment key not found" >&2
exit 1
fi
# Main deployment logic
deploy_application() {
echo "Deploying application with secure credentials..."
# Use credentials securely
export DEPLOYMENT_KEY
./deploy-app --key-file=/dev/stdin <<< "$DEPLOYMENT_KEY"
# Verify deployment
if ./verify-deployment; then
echo "Deployment successful"
else
echo "Deployment verification failed" >&2
return 1
fi
}
# Execute deployment
deploy_application
EOT
filename = "${path.module}/scripts/secure-deployment.sh"
file_permission = "0755"
}
resource "null_resource" "secure_deployment" {
depends_on = [local_file.secure_script]
provisioner "local-exec" {
command = local_file.secure_script.filename
}
}
|
Environment-Aware Scripts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35 | # environment-scripts.tf - Environment-specific shell integration
locals {
environment_configs = {
development = {
script = "scripts/dev-setup.sh"
vars = {
LOG_LEVEL = "debug"
DB_HOST = "localhost"
}
}
production = {
script = "scripts/prod-setup.sh"
vars = {
LOG_LEVEL = "info"
DB_HOST = "prod-db.cluster.amazonaws.com"
}
}
}
current_config = local.environment_configs[var.environment]
}
resource "null_resource" "environment_setup" {
provisioner "local-exec" {
command = local.current_config.script
environment = merge(
local.current_config.vars,
{
ENVIRONMENT = var.environment
TIMESTAMP = timestamp()
}
)
}
}
|
🎯 Real-World Examples
Example 1: CI/CD Integration
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43 | # cicd-integration.tf - CI/CD pipeline with shell scripts
resource "null_resource" "ci_cd_pipeline" {
triggers = {
commit_sha = var.commit_sha
}
provisioner "local-exec" {
command = <<EOT
#!/bin/bash
set -e
# CI/CD pipeline with shell scripts
echo "Starting CI/CD pipeline for commit ${var.commit_sha}"
# 1. Code checkout
git clone ${var.repo_url} /tmp/build-${var.commit_sha}
cd /tmp/build-${var.commit_sha}
git checkout ${var.commit_sha}
# 2. Build application
./scripts/build.sh
# 3. Run tests
./scripts/test.sh
# 4. Security scanning
./scripts/security-scan.sh
# 5. Deploy to staging
if [ "${var.environment}" = "staging" ]; then
./scripts/deploy-staging.sh
fi
# 6. Deploy to production (manual approval)
if [ "${var.environment}" = "production" ]; then
echo "Ready for production deployment"
# This would typically be handled by a separate approval process
fi
echo "CI/CD pipeline completed successfully"
EOT
}
}
|
Example 2: Multi-Stage Provisioning
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57 | # multi-stage.tf - Multi-stage provisioning with shell
resource "aws_instance" "multi_stage" {
ami = "ami-0c02fb55956c7d316"
instance_type = "t3.medium"
# Stage 1: Base OS setup
provisioner "remote-exec" {
inline = [
"sudo apt-get update",
"sudo apt-get install -y curl wget git",
"./scripts/base-setup.sh"
]
}
# Stage 2: Application installation
provisioner "remote-exec" {
inline = [
"./scripts/install-app.sh",
"sudo systemctl enable myapp"
]
}
# Stage 3: Configuration and validation
provisioner "remote-exec" {
inline = [
"./scripts/configure-app.sh",
"./scripts/validate-app.sh"
]
}
# Stage 4: Health check and reporting
provisioner "local-exec" {
command = <<EOT
#!/bin/bash
set -e
# Wait for application to be ready
timeout 300 bash -c 'until curl -sf http://${self.public_ip}:8080/health; do sleep 5; done'
# Send notification
curl -X POST ${var.slack_webhook} \
-H 'Content-Type: application/json' \
-d '{
"text": "Instance ${self.id} deployed successfully",
"attachments": [
{
"color": "good",
"fields": [
{"title": "Instance ID", "value": "${self.id}", "short": true},
{"title": "Public IP", "value": "${self.public_ip}", "short": true}
]
}
]
}'
EOT
}
}
|
🧪 Testing and Validation
Unit Testing Shell Scripts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85 | #!/bin/bash
# test-terraform-scripts.sh - Test framework for Terraform shell scripts
# Mock Terraform environment
setup_test_environment() {
TEST_DIR=$(mktemp -d)
cd "$TEST_DIR"
# Create mock scripts directory
mkdir -p scripts
# Mock Terraform variables
export TF_VAR_region="us-west-2"
export TF_VAR_environment="test"
}
# Test script execution
test_script_execution() {
local script_path="$1"
local expected_exit_code="${2:-0}"
echo "Testing script: $script_path"
# Execute script
if [ -f "$script_path" ]; then
local exit_code=0
"$script_path" || exit_code=$?
if [ "$exit_code" -eq "$expected_exit_code" ]; then
echo "✅ Script execution test passed"
else
echo "❌ Script execution failed with exit code $exit_code (expected $expected_exit_code)" >&2
return 1
fi
else
echo "❌ Script not found: $script_path" >&2
return 1
fi
}
# Test environment variable handling
test_environment_variables() {
local script_path="$1"
local required_vars=("$@")
echo "Testing environment variable handling for: $script_path"
# Test with missing variables
for var in "${required_vars[@]}"; do
# Temporarily unset variable
local original_value="${!var}"
unset "$var"
# Test script behavior
if "$script_path" >/dev/null 2>&1; then
echo "❌ Script should fail when $var is missing" >&2
return 1
fi
# Restore variable
if [ -n "$original_value" ]; then
export "$var"="$original_value"
fi
done
echo "✅ Environment variable handling test passed"
}
# Run all tests
run_all_tests() {
setup_test_environment
# Test individual scripts
test_script_execution "scripts/base-setup.sh"
test_script_execution "scripts/install-app.sh"
test_script_execution "scripts/configure-app.sh"
# Test environment variable handling
test_environment_variables "scripts/configure-app.sh" "DB_HOST" "DB_PASSWORD"
echo "All tests completed"
}
# Execute tests
run_all_tests
|
🧾 Summary
✅ Local-exec: Execute scripts on Terraform host
✅ Remote-exec: Execute scripts on remote resources
✅ External data: Dynamic configuration with shell scripts
✅ Conditional logic: Environment-specific provisioning
✅ Error handling: Robust retry and failure management
✅ Security: Secure credential and temporary file handling
✅ Testing: Comprehensive test frameworks for shell scripts
🧾 See Also