Przejdź do treści

🚀 Shell in Pulumi

Integrating shell scripting with Pulumi for dynamic infrastructure provisioning while leveraging modern programming language capabilities.


🎯 Pulumi Shell Integration Methods

Local Command Execution

 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
// index.ts - Local command execution in TypeScript
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as child_process from "child_process";
import * as fs from "fs";

// Execute local shell commands
function executeLocalCommand(command: string): Promise<string> {
    return new Promise((resolve, reject) => {
        child_process.exec(command, (error, stdout, stderr) => {
            if (error) {
                reject(new Error(`Command failed: ${stderr}`));
            } else {
                resolve(stdout.trim());
            }
        });
    });
}

// Generate dynamic configuration
async function generateDynamicConfig(): Promise<any> {
    const timestamp = await executeLocalCommand("date -u +%Y-%m-%dT%H:%M:%SZ");
    const hostname = await executeLocalCommand("hostname");
    const gitSha = await executeLocalCommand("git rev-parse HEAD");

    return {
        timestamp,
        hostname,
        gitSha,
        environment: process.env.ENVIRONMENT || "development"
    };
}

// Use dynamic configuration in infrastructure
const dynamicConfig = generateDynamicConfig();

const bucket = new aws.s3.Bucket("my-bucket", {
    tags: {
        CreatedAt: dynamicConfig.then(config => config.timestamp),
        CreatedBy: dynamicConfig.then(config => config.hostname),
        GitSHA: dynamicConfig.then(config => config.gitSha.substring(0, 8)),
        Environment: dynamicConfig.then(config => config.environment)
    }
});

// Export bucket information
export const bucketName = bucket.id;
export const bucketUrl = pulumi.interpolate`https://${bucket.bucketDomainName}`;

Remote Command Execution

  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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
// remote-execution.ts - Remote command execution
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as tls from "@pulumi/tls";

// Generate SSH key pair
const privateKey = new tls.PrivateKey("ssh-key", {
    algorithm: "RSA",
    rsaBits: 4096
});

// Create EC2 instance
const server = new aws.ec2.Instance("web-server", {
    ami: "ami-0c02fb55956c7d316",
    instanceType: "t3.micro",
    keyName: privateKey.id,

    userData: `#!/bin/bash
set -euo pipefail

# Install Docker
yum update -y
amazon-linux-extras install docker -y
systemctl start docker
systemctl enable docker
usermod -aG docker ec2-user

# Install application dependencies
yum install -y git nodejs npm

# Create application directory
mkdir -p /opt/myapp
`,

    tags: {
        Name: "WebServer"
    }
});

// Remote command execution using shell scripts
const provisionServer = async () => {
    const provisionScript = `#!/bin/bash
set -euo pipefail

# Wait for cloud-init to complete
until [ -f /var/lib/cloud/instance/boot-finished ]; do
  sleep 5
done

# Deploy application
cd /opt/myapp

# Clone repository
git clone https://github.com/example/myapp.git .
git checkout ${process.env.GIT_BRANCH || "main"}

# Install dependencies
npm install --production

# Create systemd service
cat > /etc/systemd/system/myapp.service << 'EOF'
[Unit]
Description=My Application
After=network.target

[Service]
Type=simple
User=ec2-user
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/node server.js
Restart=always
RestartSec=10

[Install]
WantedBy=multi-user.target
EOF

# Start service
systemctl daemon-reload
systemctl enable myapp
systemctl start myapp

# Verify service
systemctl is-active myapp

echo "Application deployed successfully"
`;

    // Execute remote commands via SSH
    const sshCommand = `
ssh -o StrictHostKeyChecking=no -i ${privateKey.privateKeyPem} \
ec2-user@${server.publicIp} << 'REMOTE_EOF'
${provisionScript}
REMOTE_EOF
`;

    await executeLocalCommand(sshCommand);
};

// Execute provisioning after instance creation
export const provisioningResult = pulumi.all([server.id, server.publicIp])
    .apply(async ([instanceId, publicIp]) => {
        if (instanceId && publicIp) {
            try {
                await provisionServer();
                return "Provisioning completed successfully";
            } catch (error) {
                throw new Error(`Provisioning failed: ${error}`);
            }
        }
        return "Waiting for instance...";
    });

🛠️ Advanced Shell Integration Patterns

Dynamic Resource Creation

 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
// dynamic-resources.ts - Dynamic resource creation with shell
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as child_process from "child_process";

// Discover available regions using shell commands
function getAvailableRegions(): Promise<string[]> {
    return new Promise((resolve, reject) => {
        child_process.exec("aws ec2 describe-regions --query 'Regions[].RegionName' --output json", (error, stdout) => {
            if (error) {
                reject(error);
            } else {
                try {
                    const regions = JSON.parse(stdout);
                    resolve(regions);
                } catch (parseError) {
                    reject(parseError);
                }
            }
        });
    });
}

// Create resources across multiple regions
async function createMultiRegionResources() {
    const regions = await getAvailableRegions();
    const resources: any[] = [];

    for (const region of regions.slice(0, 3)) {  // Limit to 3 regions for demo
        const provider = new aws.Provider(`aws-${region}`, {
            region: region as aws.Region
        });

        const bucket = new aws.s3.Bucket(`bucket-${region}`, {
            bucket: `myapp-${region}-${Date.now()}`,
            tags: {
                Region: region,
                CreatedAt: new Date().toISOString()
            }
        }, { provider });

        resources.push({
            region,
            bucketName: bucket.id
        });
    }

    return resources;
}

// Export multi-region resources
export const multiRegionResources = createMultiRegionResources();

Conditional Logic with Shell

 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
// conditional-logic.ts - Complex conditional logic
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as child_process from "child_process";

// Determine system requirements using shell commands
async function getSystemRequirements(): Promise<{ cpu: number; memory: number; storage: number }> {
    const commands = [
        "nproc",  // CPU cores
        "free -g | awk '/^Mem:/{print $2}'",  // Memory in GB
        "df -BG / | awk 'NR==2{print $4}' | sed 's/G$//'"  // Storage in GB
    ];

    const results = await Promise.all(commands.map(cmd =>
        new Promise<string>((resolve, reject) => {
            child_process.exec(cmd, (error, stdout) => {
                if (error) {
                    reject(error);
                } else {
                    resolve(stdout.trim());
                }
            });
        })
    ));

    return {
        cpu: parseInt(results[0]) || 1,
        memory: parseInt(results[1]) || 1,
        storage: parseInt(results[2]) || 10
    };
}

// Configure instance size based on system requirements
async function getInstanceSize(): Promise<aws.ec2.InstanceType> {
    const requirements = await getSystemRequirements();

    if (requirements.cpu >= 8 && requirements.memory >= 16) {
        return "m5.2xlarge";
    } else if (requirements.cpu >= 4 && requirements.memory >= 8) {
        return "m5.large";
    } else {
        return "t3.micro";
    }
}

// Create instance with dynamic sizing
const instance = new aws.ec2.Instance("dynamic-instance", {
    ami: "ami-0c02fb55956c7d316",
    instanceType: getInstanceSize(),
    tags: {
        Name: "DynamicInstance",
        CPUCores: pulumi.output(getSystemRequirements()).cpu,
        MemoryGB: pulumi.output(getSystemRequirements()).memory
    }
});

export const instanceId = instance.id;
export const instanceType = instance.instanceType;

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
// error-handling.ts - Robust error handling with retries
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as child_process from "child_process";

// Execute command with retry logic
async function executeWithRetry(
    command: string,
    maxRetries: number = 3,
    delay: number = 1000
): Promise<string> {
    let lastError: Error | undefined;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
        try {
            return await new Promise<string>((resolve, reject) => {
                child_process.exec(command, (error, stdout, stderr) => {
                    if (error) {
                        reject(new Error(`Command failed: ${stderr || error.message}`));
                    } else {
                        resolve(stdout.trim());
                    }
                });
            });
        } catch (error) {
            lastError = error as Error;

            if (attempt < maxRetries) {
                console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
                await new Promise(resolve => setTimeout(resolve, delay));
                delay *= 2;  // Exponential backoff
            }
        }
    }

    throw new Error(`All ${maxRetries} attempts failed: ${lastError?.message}`);
}

// Create S3 bucket with retry logic
const createBucketWithRetry = async (): Promise<aws.s3.Bucket> => {
    try {
        // Pre-flight check using shell command
        await executeWithRetry("aws sts get-caller-identity");

        // Create bucket
        const bucket = new aws.s3.Bucket("retry-bucket", {
            bucket: `myapp-retry-${Date.now()}`,
            tags: {
                CreatedAt: new Date().toISOString(),
                RetryAttempts: "handled"
            }
        });

        // Post-creation validation
        await executeWithRetry(`aws s3 ls s3://${bucket.id}`);

        return bucket;
    } catch (error) {
        throw new Error(`Failed to create bucket after retries: ${error}`);
    }
};

// Export bucket with retry handling
export const retryBucket = pulumi.output(createBucketWithRetry());
export const bucketName = retryBucket.id;

📋 Shell Script Best Practices in Pulumi

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
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
// secure-scripts.ts - Secure shell script management
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";
import * as crypto from "crypto";

// Create secure temporary script
class SecureScriptManager {
    private tempDir: string;
    private scripts: Map<string, string> = new Map();

    constructor() {
        this.tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulumi-secure-'));
    }

    createSecureScript(name: string, content: string): string {
        const scriptPath = path.join(this.tempDir, `${name}-${crypto.randomBytes(8).toString('hex')}.sh`);

        // Write script with secure permissions
        fs.writeFileSync(scriptPath, `#!/bin/bash\nset -euo pipefail\n\n${content}`, { mode: 0o700 });

        this.scripts.set(name, scriptPath);
        return scriptPath;
    }

    cleanup(): void {
        // Clean up temporary scripts
        this.scripts.forEach((scriptPath) => {
            if (fs.existsSync(scriptPath)) {
                fs.unlinkSync(scriptPath);
            }
        });

        if (fs.existsSync(this.tempDir)) {
            fs.rmdirSync(this.tempDir, { recursive: true });
        }
    }
}

// Use secure script manager
const scriptManager = new SecureScriptManager();

// Secure deployment script
const deployScript = scriptManager.createSecureScript("deploy", `
# Secure deployment script
set -euo pipefail

# Validate inputs
if [ -z "${process.env.DEPLOY_KEY:-}" ]; then
    echo "Error: DEPLOY_KEY not set" >&2
    exit 1
fi

# Create secure temporary directory
SECURE_TEMP=$(mktemp -d)
trap 'rm -rf "$SECURE_TEMP"' EXIT

# Deploy application securely
echo "Deploying application..."
cd "$SECURE_TEMP"

# Download and verify artifact
curl -s -H "Authorization: Bearer $DEPLOY_KEY" \\
    "https://artifacts.example.com/app.tar.gz" | \\
    tee app.tar.gz | \\
    sha256sum > app.sha256

# Verify checksum
if ! sha256sum -c app.sha256; then
    echo "Artifact verification failed" >&2
    exit 1
fi

# Extract and deploy
tar -xzf app.tar.gz
# ... deployment logic ...

echo "Deployment completed successfully"
`);

// Execute secure script
const executeSecureScript = async (): Promise<void> => {
    try {
        await new Promise<void>((resolve, reject) => {
            const child = child_process.spawn(deployScript, {
                env: {
                    ...process.env,
                    DEPLOY_KEY: process.env.DEPLOY_KEY || ""
                }
            });

            child.stdout.on('data', (data) => {
                console.log(data.toString());
            });

            child.stderr.on('data', (data) => {
                console.error(data.toString());
            });

            child.on('close', (code) => {
                if (code === 0) {
                    resolve();
                } else {
                    reject(new Error(`Script failed with exit code ${code}`));
                }
            });
        });
    } finally {
        scriptManager.cleanup();
    }
};

// Export deployment result
export const deploymentStatus = pulumi.output(executeSecureScript())
    .apply(() => "Deployment successful")
    .catch((error) => `Deployment failed: ${error.message}`);

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
 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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
// environment-aware.ts - Environment-specific shell integration
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as child_process from "child_process";

// Environment configuration
interface EnvironmentConfig {
    instanceType: aws.ec2.InstanceType;
    ami: string;
    tags: Record<string, string>;
    userDataScript: string;
}

const environmentConfigs: Record<string, EnvironmentConfig> = {
    development: {
        instanceType: "t3.micro",
        ami: "ami-0c02fb55956c7d316",
        tags: { Environment: "development", CostCenter: "dev" },
        userDataScript: `#!/bin/bash
# Development environment setup
yum update -y
yum install -y docker git
systemctl start docker
systemctl enable docker
usermod -aG docker ec2-user
`
    },
    staging: {
        instanceType: "t3.small",
        ami: "ami-0c02fb55956c7d316",
        tags: { Environment: "staging", CostCenter: "qa" },
        userDataScript: `#!/bin/bash
# Staging environment setup
yum update -y
yum install -y docker git nginx
systemctl start docker nginx
systemctl enable docker nginx
usermod -aG docker ec2-user

# Configure monitoring
yum install -y amazon-cloudwatch-agent
`
    },
    production: {
        instanceType: "t3.medium",
        ami: "ami-0c02fb55956c7d316",
        tags: { Environment: "production", CostCenter: "prod", Critical: "true" },
        userDataScript: `#!/bin/bash
# Production environment setup
yum update -y
yum install -y docker git nginx
systemctl start docker nginx
systemctl enable docker nginx
usermod -aG docker ec2-user

# Configure security
yum install -y fail2ban
systemctl start fail2ban
systemctl enable fail2ban

# Configure monitoring and alerting
yum install -y amazon-cloudwatch-agent
`
    }
};

// Get environment configuration
const environment = process.env.ENVIRONMENT || "development";
const config = environmentConfigs[environment] || environmentConfigs.development;

// Create environment-specific instance
const server = new aws.ec2.Instance(`${environment}-server`, {
    ami: config.ami,
    instanceType: config.instanceType,
    userData: config.userDataScript,
    tags: {
        ...config.tags,
        Name: `${environment}-server`,
        CreatedAt: new Date().toISOString()
    }
});

// Environment-specific provisioning
const provisionEnvironment = async (): Promise<string> => {
    const provisionScript = `#!/bin/bash
set -euo pipefail

# Wait for instance to be ready
sleep 60

# Environment-specific provisioning
case "${environment}" in
    production)
        echo "Running production-specific provisioning..."
        # Production-specific steps
        aws s3 cp s3://prod-config/${environment}/config.json /etc/app/
        systemctl restart app
        ;;
    staging)
        echo "Running staging-specific provisioning..."
        # Staging-specific steps
        aws s3 cp s3://staging-config/${environment}/config.json /etc/app/
        systemctl restart app
        ;;
    development)
        echo "Running development-specific provisioning..."
        # Development-specific steps
        curl -o /etc/app/config.json https://dev-config.example.com/config.json
        systemctl restart app
        ;;
esac

echo "Environment ${environment} provisioned successfully"
`;

    try {
        await new Promise<void>((resolve, reject) => {
            child_process.exec(provisionScript, (error, stdout, stderr) => {
                if (error) {
                    reject(new Error(`Provisioning failed: ${stderr}`));
                } else {
                    console.log(stdout);
                    resolve();
                }
            });
        });

        return "Provisioning completed successfully";
    } catch (error) {
        throw new Error(`Environment provisioning failed: ${error}`);
    }
};

// Export environment resources
export const instanceId = server.id;
export const publicIp = server.publicIp;
export const environmentStatus = pulumi.output(provisionEnvironment());

🎯 Real-World Examples

Example 1: CI/CD Pipeline 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
 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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
// cicd-pipeline.ts - CI/CD pipeline with shell scripts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as child_process from "child_process";

// CI/CD pipeline configuration
interface PipelineConfig {
    gitRepo: string;
    gitBranch: string;
    buildScript: string;
    testScript: string;
    deployScript: string;
}

const pipelineConfig: PipelineConfig = {
    gitRepo: process.env.GIT_REPO || "https://github.com/example/app.git",
    gitBranch: process.env.GIT_BRANCH || "main",
    buildScript: `#!/bin/bash
set -euo pipefail

echo "Building application..."
cd /tmp/build

# Clone repository
git clone --depth 1 --branch ${process.env.GIT_BRANCH || "main"} ${process.env.GIT_REPO} .

# Install dependencies
npm install

# Build application
npm run build

# Create artifact
tar -czf app-artifact.tar.gz dist/
`,
    testScript: `#!/bin/bash
set -euo pipefail

echo "Running tests..."
cd /tmp/build

# Run unit tests
npm test

# Run integration tests
npm run test:integration

# Run security scans
npm audit
`,
    deployScript: `#!/bin/bash
set -euo pipefail

echo "Deploying application..."
ARTIFACT_URL="${process.env.ARTIFACT_URL}"

# Download artifact
curl -L "$ARTIFACT_URL" | tar -xz -C /opt/app

# Install dependencies
cd /opt/app
npm install --production

# Restart service
systemctl restart app
systemctl is-active app

echo "Deployment completed successfully"
`
};

// Create CI/CD pipeline
class CICDPipeline {
    private pipelineRole: aws.iam.Role;
    private codeBuildProject: aws.codebuild.Project;
    private codePipeline: aws.codepipeline.Pipeline;

    constructor(name: string, config: PipelineConfig) {
        // Create IAM role for pipeline
        this.pipelineRole = new aws.iam.Role(`${name}-role`, {
            assumeRolePolicy: {
                Version: "2012-10-17",
                Statement: [{
                    Effect: "Allow",
                    Principal: {
                        Service: "codepipeline.amazonaws.com"
                    },
                    Action: "sts:AssumeRole"
                }]
            }
        });

        // Create CodeBuild project
        this.codeBuildProject = new aws.codebuild.Project(`${name}-build`, {
            name: `${name}-build`,
            serviceRole: this.pipelineRole.arn,
            artifacts: {
                type: "CODEPIPELINE"
            },
            environment: {
                computeType: "BUILD_GENERAL1_SMALL",
                image: "aws/codebuild/standard:5.0",
                type: "LINUX_CONTAINER"
            },
            source: {
                type: "CODEPIPELINE",
                buildspec: `version: 0.2
phases:
  install:
    runtime-versions:
      nodejs: 16
  pre_build:
    commands:
      - echo "Preparing build environment..."
  build:
    commands:
      - echo "Running build script..."
      - |
${config.buildScript.split('\n').map(line => `        ${line}`).join('\n')}
  post_build:
    commands:
      - echo "Build completed"
artifacts:
  files:
    - '**/*'
  discard-paths: no
`
            }
        });

        // Create CodePipeline
        this.codePipeline = new aws.codepipeline.Pipeline(`${name}-pipeline`, {
            name: `${name}-pipeline`,
            roleArn: this.pipelineRole.arn,
            artifactStore: {
                location: new aws.s3.Bucket(`${name}-artifacts`, {
                    bucket: `${name}-artifacts-${Date.now()}`
                }).bucket,
                type: "S3"
            },
            stages: [
                {
                    name: "Source",
                    actions: [{
                        name: "Source",
                        category: "Source",
                        owner: "ThirdParty",
                        provider: "GitHub",
                        version: "1",
                        outputArtifacts: ["source"],
                        configuration: {
                            Owner: "example",
                            Repo: "app",
                            Branch: config.gitBranch,
                            OAuthToken: process.env.GITHUB_TOKEN
                        }
                    }]
                },
                {
                    name: "Build",
                    actions: [{
                        name: "Build",
                        category: "Build",
                        owner: "AWS",
                        provider: "CodeBuild",
                        version: "1",
                        inputArtifacts: ["source"],
                        outputArtifacts: ["build"],
                        configuration: {
                            ProjectName: this.codeBuildProject.name
                        }
                    }]
                }
            ]
        });
    }

    getPipelineUrl(): pulumi.Output<string> {
        return pulumi.interpolate`https://console.aws.amazon.com/codepipeline/home?region=${aws.config.region}#/view/${this.codePipeline.name}`;
    }
}

// Create CI/CD pipeline
const pipeline = new CICDPipeline("myapp", pipelineConfig);

// Export pipeline information
export const pipelineUrl = pipeline.getPipelineUrl();
export const pipelineName = pipeline.codePipeline.name;

Example 2: Multi-Stage Application Deployment

  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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
// multi-stage-deployment.ts - Multi-stage deployment
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import * as child_process from "child_process";

// Multi-stage deployment configuration
interface DeploymentStage {
    name: string;
    preChecks: string[];
    deploymentScript: string;
    postChecks: string[];
    rollbackScript?: string;
}

const deploymentStages: DeploymentStage[] = [
    {
        name: "prepare",
        preChecks: [
            "df -h / | awk 'NR==2 { if($5+0 > 80) exit 1 }'",
            "free | awk 'NR==2 { if($4/($2/100) < 10) exit 1 }'"
        ],
        deploymentScript: `#!/bin/bash
set -euo pipefail

echo "=== Stage: Prepare ==="

# Create deployment directories
mkdir -p /var/www/app/{current,staging,backups}
chown -R www-data:www-data /var/www/app

# Create backup of current deployment
if [ -d /var/www/app/current ]; then
    BACKUP_NAME="backup-$(date +%Y%m%d-%H%M%S)"
    cp -r /var/www/app/current /var/www/app/backups/$BACKUP_NAME
    echo "Backup created: $BACKUP_NAME"
fi

# Prepare staging directory
rm -rf /var/www/app/staging
mkdir -p /var/www/app/staging
`,
        postChecks: [
            "test -d /var/www/app/staging",
            "test -w /var/www/app/staging"
        ]
    },
    {
        name: "deploy",
        preChecks: [
            "curl -sf https://artifacts.example.com/app.tar.gz >/dev/null"
        ],
        deploymentScript: `#!/bin/bash
set -euo pipefail

echo "=== Stage: Deploy ==="

# Download and extract application
cd /var/www/app/staging
curl -L https://artifacts.example.com/app.tar.gz | tar -xz

# Install dependencies
if [ -f package.json ]; then
    npm install --production
fi

# Set permissions
chown -R www-data:www-data /var/www/app/staging
find /var/www/app/staging -type f -exec chmod 644 {} \\;
find /var/www/app/staging -type d -exec chmod 755 {} \\;

# Create application symlink
ln -sf /var/www/app/staging /var/www/app/new
`,
        postChecks: [
            "test -f /var/www/app/staging/package.json",
            "test -d /var/www/app/new"
        ]
    },
    {
        name: "test",
        preChecks: [],
        deploymentScript: `#!/bin/bash
set -euo pipefail

echo "=== Stage: Test ==="

# Run application tests
cd /var/www/app/staging

# Unit tests
if [ -f tests/unit.sh ]; then
    ./tests/unit.sh
fi

# Integration tests
if [ -f tests/integration.sh ]; then
    ./tests/integration.sh
fi

# Security checks
if command -v npm >/dev/null; then
    npm audit --audit-level high
fi
`,
        postChecks: [
            "curl -sf http://localhost/health >/dev/null"
        ],
        rollbackScript: `#!/bin/bash
set -euo pipefail

echo "=== Rollback: Test Failed ==="

# Stop application
systemctl stop app || true

# Restore from backup
LATEST_BACKUP=$(ls -td /var/www/app/backups/*/ 2>/dev/null | head -1)
if [ -n "$LATEST_BACKUP" ]; then
    rm -rf /var/www/app/current
    cp -r "$LATEST_BACKUP" /var/www/app/current
    systemctl start app
fi
`
    },
    {
        name: "activate",
        preChecks: [],
        deploymentScript: `#!/bin/bash
set -euo pipefail

echo "=== Stage: Activate ==="

# Atomic deployment switch
mv /var/www/app/new /var/www/app/current-tmp
ln -sfn /var/www/app/current-tmp /var/www/app/current
rm -rf /var/www/app/current-tmp

# Restart services
systemctl restart nginx
systemctl restart app

# Verify services are running
systemctl is-active nginx
systemctl is-active app

# Warm up cache
curl -s http://localhost/warmup >/dev/null || true
`,
        postChecks: [
            "systemctl is-active app",
            "curl -sf http://localhost/ >/dev/null"
        ]
    },
    {
        name: "cleanup",
        preChecks: [],
        deploymentScript: `#!/bin/bash
set -euo pipefail

echo "=== Stage: Cleanup ==="

# Clean up old backups (keep last 5)
ls -td /var/www/app/backups/*/ 2>/dev/null | tail -n +6 | xargs rm -rf

# Clean up temporary files
find /tmp -name "deploy-*" -mtime +1 -delete 2>/dev/null || true

# Send deployment notification
curl -X POST "${process.env.SLACK_WEBHOOK}" \
  -H "Content-Type: application/json" \
  -d '{
    "text": "Deployment completed successfully",
    "attachments": [{
      "color": "good",
      "fields": [
        {"title": "Application", "value": "MyApp", "short": true},
        {"title": "Version", "value": "${process.env.APP_VERSION || 'latest'}", "short": true},
        {"title": "Environment", "value": "${process.env.ENVIRONMENT || 'unknown'}", "short": true},
        {"title": "Timestamp", "value": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'", "short": true}
      ]
    }]
  }' || true
`,
        postChecks: []
    }
];

// Execute multi-stage deployment
class MultiStageDeployment {
    private stages: DeploymentStage[];
    private results: Map<string, string> = new Map();

    constructor(stages: DeploymentStage[]) {
        this.stages = stages;
    }

    async execute(): Promise<Map<string, string>> {
        for (const stage of this.stages) {
            try {
                console.log(`Executing stage: ${stage.name}`);

                // Run pre-checks
                for (const check of stage.preChecks) {
                    await this.executeCommand(check);
                }

                // Execute deployment script
                await this.executeCommand(stage.deploymentScript);

                // Run post-checks
                for (const check of stage.postChecks) {
                    await this.executeCommand(check);
                }

                this.results.set(stage.name, "completed");
                console.log(`Stage ${stage.name} completed successfully`);

            } catch (error) {
                console.error(`Stage ${stage.name} failed: ${error}`);

                // Execute rollback if available
                if (stage.rollbackScript) {
                    try {
                        await this.executeCommand(stage.rollbackScript);
                        console.log(`Rollback for stage ${stage.name} completed`);
                    } catch (rollbackError) {
                        console.error(`Rollback for stage ${stage.name} failed: ${rollbackError}`);
                    }
                }

                this.results.set(stage.name, `failed: ${error}`);
                throw new Error(`Deployment failed at stage ${stage.name}: ${error}`);
            }
        }

        return this.results;
    }

    private executeCommand(command: string): Promise<void> {
        return new Promise((resolve, reject) => {
            const child = child_process.exec(command, { timeout: 300000 }); // 5 minute timeout

            child.stdout?.on('data', (data) => {
                console.log(data.toString());
            });

            child.stderr?.on('data', (data) => {
                console.error(data.toString());
            });

            child.on('close', (code) => {
                if (code === 0) {
                    resolve();
                } else {
                    reject(new Error(`Command failed with exit code ${code}`));
                }
            });

            child.on('error', (error) => {
                reject(error);
            });
        });
    }
}

// Create and execute multi-stage deployment
const deployment = new MultiStageDeployment(deploymentStages);

// Export deployment results
export const deploymentResults = pulumi.output(deployment.execute());
export const deploymentStatus = deploymentResults.apply(results => {
    const failedStages = Array.from(results.entries())
        .filter(([_, status]) => status.includes('failed'))
        .map(([stage, _]) => stage);

    return failedStages.length === 0 ? "successful" : `failed at stages: ${failedStages.join(', ')}`;
});

🧪 Testing and Validation

Unit Testing Shell Scripts in Pulumi

  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
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
// test-pulumi-scripts.ts - Test framework for Pulumi shell scripts
import * as child_process from "child_process";
import * as fs from "fs";
import * as os from "os";
import * as path from "path";

// Test environment setup
class TestEnvironment {
    private tempDir: string;

    constructor() {
        this.tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'pulumi-test-'));
    }

    createTestScript(content: string): string {
        const scriptPath = path.join(this.tempDir, `test-${Date.now()}.sh`);
        fs.writeFileSync(scriptPath, `#!/bin/bash\nset -euo pipefail\n\n${content}`, { mode: 0o755 });
        return scriptPath;
    }

    cleanup(): void {
        if (fs.existsSync(this.tempDir)) {
            fs.rmSync(this.tempDir, { recursive: true, force: true });
        }
    }
}

// Test framework
class ShellScriptTester {
    private testEnv: TestEnvironment;

    constructor() {
        this.testEnv = new TestEnvironment();
    }

    async testScriptExecution(scriptContent: string, expectedExitCode: number = 0): Promise<boolean> {
        const scriptPath = this.testEnv.createTestScript(scriptContent);

        return new Promise<boolean>((resolve) => {
            child_process.execFile(scriptPath, (error, stdout, stderr) => {
                const exitCode = error ? (error as any).code : 0;

                if (exitCode === expectedExitCode) {
                    console.log('✅ Script execution test passed');
                    resolve(true);
                } else {
                    console.error(`❌ Script execution failed with exit code ${exitCode} (expected ${expectedExitCode})`);
                    console.error('Stderr:', stderr);
                    resolve(false);
                }
            });
        });
    }

    async testEnvironmentVariables(scriptContent: string, requiredVars: string[]): Promise<boolean> {
        const scriptPath = this.testEnv.createTestScript(scriptContent);

        // Test with missing variables
        for (const varName of requiredVars) {
            const env = { ...process.env };
            delete env[varName];

            return new Promise<boolean>((resolve) => {
                const child = child_process.spawn(scriptPath, [], { env });

                child.on('close', (code) => {
                    if (code !== 0) {
                        console.log('✅ Environment variable handling test passed');
                        resolve(true);
                    } else {
                        console.error(`❌ Script should fail when ${varName} is missing`);
                        resolve(false);
                    }
                });
            });
        }

        return true;
    }

    async runAllTests(): Promise<void> {
        console.log('Running all shell script tests...');

        // Test basic script execution
        await this.testScriptExecution('echo "Hello, World!"', 0);
        await this.testScriptExecution('exit 1', 1);

        // Test environment variable handling
        const envScript = `
if [ -z "$REQUIRED_VAR" ]; then
    echo "REQUIRED_VAR is not set" >&2
    exit 1
fi
echo "REQUIRED_VAR is set to: $REQUIRED_VAR"
`;
        await this.testEnvironmentVariables(envScript, ['REQUIRED_VAR']);

        console.log('All tests completed');
        this.testEnv.cleanup();
    }
}

// Run tests
const tester = new ShellScriptTester();
tester.runAllTests().catch(console.error);

🧾 Summary

Local execution: Execute shell commands on Pulumi host ✅ Remote execution: Run commands on remote resources ✅ Dynamic resource creation: Generate resources based on shell command output ✅ Conditional logic: Complex decision-making with shell integration ✅ Error handling: Robust retry logic and failure management ✅ Security: Secure credential and temporary file handling ✅ Environment awareness: Context-specific configuration ✅ Testing: Comprehensive test frameworks for shell scripts


🧾 See Also