Skip to main content

CloudForge Application Plugin Development Guide

Overview

CloudForge provides a powerful plugin system for custom applications, enabling developers to:

  • ✅ Deploy any application on AWS using CloudForge infrastructure
  • ✅ Support both Docker/ECS (Fargate) and EC2 deployments automatically
  • ✅ Distribute applications as standalone JAR files
  • ✅ Integrate with CloudForge's security, compliance, and OIDC systems
  • ✅ Reuse battle-tested infrastructure patterns (VPC, ALB, EFS, monitoring)

Quick Start

1. Create a New Maven Project

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>

<groupId>com.example</groupId>
<artifactId>vault-application</artifactId>
<version>1.0.0</version>
<name>HashiCorp Vault Application for CloudForge</name>

<dependencies>
<!-- CloudForge Core API -->
<dependency>
<groupId>com.cloudforgeci</groupId>
<artifactId>cloudforge-core</artifactId>
<version>3.0.0</version>
<scope>provided</scope>
</dependency>

<!-- CloudForge API (for ApplicationFactory) -->
<dependency>
<groupId>com.cloudforgeci</groupId>
<artifactId>cloudforge-api</artifactId>
<version>3.0.0</version>
<scope>provided</scope>
</dependency>

<!-- AWS CDK -->
<dependency>
<groupId>software.amazon.awscdk</groupId>
<artifactId>aws-cdk-lib</artifactId>
<version>2.147.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>

2. Implement ApplicationSpec

package com.example.applications;

import com.cloudforge.core.interfaces.ApplicationSpec;
import com.cloudforge.core.interfaces.Ec2Context;
import com.cloudforge.core.interfaces.OidcIntegration;
import com.cloudforge.core.interfaces.UserDataBuilder;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* HashiCorp Vault ApplicationSpec implementation.
*
* <p>Deploys HashiCorp Vault for secrets management and encryption on AWS.</p>
*
* <h2>Features:</h2>
* <ul>
* <li>Secrets management and encryption as a service</li>
* <li>Dynamic secrets for databases and cloud providers</li>
* <li>Centralized secrets storage</li>
* <li>Audit logging and access control</li>
* <li>KMS auto-unseal integration</li>
* </ul>
*
* <h2>Deployment Modes:</h2>
* <ul>
* <li><b>Fargate (Container):</b> Uses official vault:latest Docker image</li>
* <li><b>EC2:</b> Installs Vault binary via HashiCorp repository</li>
* </ul>
*
* @since 1.0.0
*/
public class VaultApplicationSpec implements ApplicationSpec {

// ========== Application Identity ==========

@Override
public String applicationId() {
return "vault";
}

// ========== Container Configuration (Fargate) ==========

@Override
public String defaultContainerImage() {
return "hashicorp/vault:latest";
}

@Override
public int applicationPort() {
return 8200; // Vault HTTP API port
}

@Override
public String containerDataPath() {
return "/vault/data";
}

@Override
public String efsDataPath() {
return "/vault";
}

@Override
public String volumeName() {
return "vaultData";
}

@Override
public String containerUser() {
return "100:1000"; // Vault runs as user 100
}

@Override
public String efsPermissions() {
return "755";
}

@Override
public String healthCheckPath() {
return "/v1/sys/health?standbyok=true";
}

@Override
public Map<String, String> containerEnvironmentVariables(String fqdn, boolean sslEnabled, String authMode) {
Map<String, String> environment = new HashMap<>();

// Vault server configuration
environment.put("VAULT_API_ADDR", (sslEnabled ? "https://" : "http://") + (fqdn != null ? fqdn : "localhost:8200"));
environment.put("VAULT_ADDR", "http://127.0.0.1:8200"); // Internal communication
environment.put("SKIP_SETCAP", "true"); // Required for container environments

// Enable Vault UI
environment.put("VAULT_UI", "true");

// Vault log level
environment.put("VAULT_LOG_LEVEL", "info");

return environment;
}

// ========== EC2 Configuration ==========

@Override
public String ebsDeviceName() {
return "/dev/xvdh";
}

@Override
public String ec2DataPath() {
return "/opt/vault/data";
}

@Override
public List<String> ec2LogPaths() {
return List.of(
"/var/log/vault/vault.log",
"/var/log/vault/audit.log",
"/var/log/userdata.log",
"/var/log/messages"
);
}

@Override
public void configureUserData(UserDataBuilder builder, Ec2Context context) {
// System updates
builder.addSystemUpdate();

// Install required packages
builder.addCommands(
"# Install dependencies",
"command -v dnf >/dev/null && dnf -y install yum-utils || yum -y install yum-utils",
"echo 'Dependencies installed' >> /var/log/userdata.log"
);

// Add HashiCorp repository
builder.addCommands(
"# Add HashiCorp repository",
"yum-config-manager --add-repo https://rpm.releases.hashicorp.com/AmazonLinux/hashicorp.repo",
"echo 'HashiCorp repository added' >> /var/log/userdata.log"
);

// Install Vault
builder.addCommands(
"# Install Vault",
"command -v dnf >/dev/null && dnf -y install vault || yum -y install vault",
"echo 'Vault installed' >> /var/log/userdata.log"
);

// Install and configure CloudWatch Agent
String logGroupName = String.format("/aws/%s/%s/%s",
context.stackName(),
context.runtimeType(),
context.securityProfile());
builder.installCloudWatchAgent(logGroupName, ec2LogPaths());

// Mount storage (EFS or EBS based on availability)
String[] userParts = containerUser().split(":");
String uid = userParts[0];
String gid = userParts[1];

if (context.hasEfs()) {
builder.mountEfs(
context.efsId().orElseThrow(),
context.accessPointId().orElseThrow(),
ec2DataPath(),
uid,
gid
);
} else {
builder.mountEbs(
ebsDeviceName(),
ec2DataPath(),
uid,
gid
);
}

// Create Vault directories
builder.addCommands(
"# Create Vault directories",
"mkdir -p /etc/vault.d",
"mkdir -p /var/log/vault",
"chown -R vault:vault /var/log/vault",
"echo 'Vault directories created' >> /var/log/userdata.log"
);

// Configure Vault
builder.addCommands(
"# Configure Vault",
"cat > /etc/vault.d/vault.hcl <<'EOF'",
"ui = true",
"",
"storage \"file\" {",
" path = \"" + ec2DataPath() + "\"",
"}",
"",
"listener \"tcp\" {",
" address = \"0.0.0.0:8200\"",
" tls_disable = 1",
"}",
"",
"api_addr = \"http://127.0.0.1:8200\"",
"EOF",
"chown vault:vault /etc/vault.d/vault.hcl",
"chmod 640 /etc/vault.d/vault.hcl",
"echo 'Vault configuration created' >> /var/log/userdata.log"
);

// Configure Vault systemd service
builder.addCommands(
"# Configure Vault systemd service",
"cat > /etc/systemd/system/vault.service <<'EOF'",
"[Unit]",
"Description=HashiCorp Vault",
"Documentation=https://www.vaultproject.io/docs/",
"Requires=network-online.target",
"After=network-online.target",
"ConditionFileNotEmpty=/etc/vault.d/vault.hcl",
"",
"[Service]",
"User=vault",
"Group=vault",
"ProtectSystem=full",
"ProtectHome=read-only",
"PrivateTmp=yes",
"PrivateDevices=yes",
"SecureBits=keep-caps",
"AmbientCapabilities=CAP_IPC_LOCK",
"CapabilityBoundingSet=CAP_SYSLOG CAP_IPC_LOCK",
"NoNewPrivileges=yes",
"ExecStart=/usr/bin/vault server -config=/etc/vault.d/vault.hcl",
"ExecReload=/bin/kill --signal HUP $MAINPID",
"KillMode=process",
"KillSignal=SIGINT",
"Restart=on-failure",
"RestartSec=5",
"TimeoutStopSec=30",
"LimitNOFILE=65536",
"LimitMEMLOCK=infinity",
"",
"[Install]",
"WantedBy=multi-user.target",
"EOF",
"echo 'Vault systemd service configured' >> /var/log/userdata.log"
);

// Start Vault
builder.addCommands(
"# Start Vault",
"systemctl daemon-reload",
"systemctl enable vault",
"systemctl start vault",
"echo 'Vault service started' >> /var/log/userdata.log",
"",
"# Wait for Vault to start",
"sleep 10",
"",
"# Check Vault status",
"if systemctl is-active --quiet vault; then",
" echo 'Vault is running' >> /var/log/userdata.log",
" export VAULT_ADDR='http://127.0.0.1:8200'",
" vault status >> /var/log/userdata.log 2>&1 || echo 'Vault not initialized yet' >> /var/log/userdata.log",
"else",
" echo 'ERROR: Vault failed to start' >> /var/log/userdata.log",
" journalctl -u vault -n 50 >> /var/log/userdata.log",
"fi"
);
}

// ========== OIDC Integration (Not supported by Vault Community) ==========

@Override
public boolean supportsOidcIntegration() {
return false; // OIDC is an Enterprise feature
}

@Override
public OidcIntegration getOidcIntegration() {
return null;
}

@Override
public String toString() {
return "VaultApplicationSpec{" +
"applicationId='vault'" +
", defaultImage='hashicorp/vault:latest'" +
", applicationPort=8200" +
", containerDataPath='/vault/data'" +
", ec2DataPath='/opt/vault/data'" +
'}';
}
}

3. Register Your Application (ServiceLoader Pattern)

Create: src/main/resources/META-INF/services/com.cloudforge.core.interfaces.ApplicationSpec

# HashiCorp Vault Application
com.example.applications.VaultApplicationSpec

4. Build and Distribute

mvn clean package

Using Your Custom Application

In CDK Stack

import com.cloudforge.core.interfaces.ApplicationSpec;
import com.example.applications.VaultApplicationSpec;
import com.cloudforgeci.api.compute.ApplicationFactory;
import com.cloudforge.core.enums.RuntimeType;

public class VaultStack extends Stack {
public VaultStack(Construct scope, String id) {
super(scope, id);

// Create application spec
ApplicationSpec vaultSpec = new VaultApplicationSpec();

// Deploy on Fargate
ApplicationFactory vaultFargate = new ApplicationFactory(
this,
"VaultFargate",
RuntimeType.FARGATE,
vaultSpec
);

// Or deploy on EC2
ApplicationFactory vaultEc2 = new ApplicationFactory(
this,
"VaultEc2",
RuntimeType.EC2,
vaultSpec
);
}
}

Via cdk.json Configuration

{
"context": {
"stackName": "VaultProd",
"applicationId": "vault",
"runtimeType": "FARGATE",
"securityProfile": "PRODUCTION",
"domain": "example.com",
"subdomain": "vault",
"sslEnabled": "true"
}
}

Advanced Examples

Example 1: GitLab with OIDC Integration

package com.example.applications;

import com.cloudforge.core.interfaces.ApplicationSpec;
import com.cloudforge.core.interfaces.Ec2Context;
import com.cloudforge.core.interfaces.OidcIntegration;
import com.cloudforge.core.interfaces.UserDataBuilder;
import com.cloudforge.core.oidc.GitLabOidcIntegration;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
* GitLab ApplicationSpec implementation with OIDC support.
*/
public class GitLabApplicationSpec implements ApplicationSpec {

@Override
public String applicationId() {
return "gitlab";
}

@Override
public String defaultContainerImage() {
return "gitlab/gitlab-ce:latest";
}

@Override
public int applicationPort() {
return 80;
}

@Override
public String containerDataPath() {
return "/var/opt/gitlab";
}

@Override
public String efsDataPath() {
return "/gitlab";
}

@Override
public String volumeName() {
return "gitlabData";
}

@Override
public String containerUser() {
return "998:998"; // GitLab git user
}

@Override
public String efsPermissions() {
return "755";
}

@Override
public String healthCheckPath() {
return "/-/health";
}

@Override
public Map<String, String> containerEnvironmentVariables(String fqdn, boolean sslEnabled, String authMode) {
Map<String, String> environment = new HashMap<>();

// GitLab configuration
StringBuilder omnibusConfig = new StringBuilder();

// External URL
if (fqdn != null && !fqdn.isBlank()) {
String externalUrl = (sslEnabled ? "https://" : "http://") + fqdn;
omnibusConfig.append("external_url '").append(externalUrl).append("'; ");
}

// OIDC configuration for application-oidc mode
if ("application-oidc".equals(authMode)) {
omnibusConfig.append("gitlab_rails['omniauth_enabled'] = true; ");
omnibusConfig.append("gitlab_rails['omniauth_allow_single_sign_on'] = ['openid_connect']; ");
omnibusConfig.append("gitlab_rails['omniauth_block_auto_created_users'] = false; ");
omnibusConfig.append("gitlab_rails['omniauth_auto_link_user'] = ['openid_connect']; ");
}

// Disable HTTPS redirect (ALB handles SSL)
if (sslEnabled) {
omnibusConfig.append("nginx['listen_https'] = false; ");
omnibusConfig.append("nginx['listen_port'] = 80; ");
}

environment.put("GITLAB_OMNIBUS_CONFIG", omnibusConfig.toString().trim());

return environment;
}

@Override
public String ebsDeviceName() {
return "/dev/xvdh";
}

@Override
public String ec2DataPath() {
return "/var/opt/gitlab";
}

@Override
public List<String> ec2LogPaths() {
return List.of(
"/var/log/gitlab/gitlab-rails/production.log",
"/var/log/gitlab/nginx/gitlab_access.log",
"/var/log/userdata.log"
);
}

@Override
public void configureUserData(UserDataBuilder builder, Ec2Context context) {
// System updates
builder.addSystemUpdate();

// Install dependencies
builder.addCommands(
"# Install dependencies for GitLab",
"command -v dnf >/dev/null && dnf -y install curl policycoreutils openssh-server perl postfix || " +
"yum -y install curl policycoreutils openssh-server perl postfix",
"systemctl enable sshd postfix",
"systemctl start sshd postfix",
"echo 'Dependencies installed' >> /var/log/userdata.log"
);

// Add GitLab repository
builder.addCommands(
"# Add GitLab repository",
"curl https://packages.gitlab.com/install/repositories/gitlab/gitlab-ce/script.rpm.sh | bash",
"echo 'GitLab repository added' >> /var/log/userdata.log"
);

// Install GitLab
builder.addCommands(
"# Install GitLab CE",
"EXTERNAL_URL=\"http://" + context.fqdn().orElse("localhost") + "\" yum -y install gitlab-ce",
"echo 'GitLab installed' >> /var/log/userdata.log"
);

// Install and configure CloudWatch Agent
String logGroupName = String.format("/aws/%s/%s/%s",
context.stackName(),
context.runtimeType(),
context.securityProfile());
builder.installCloudWatchAgent(logGroupName, ec2LogPaths());

// Mount storage (EFS or EBS)
String[] userParts = containerUser().split(":");
String uid = userParts[0];
String gid = userParts[1];

if (context.hasEfs()) {
builder.mountEfs(
context.efsId().orElseThrow(),
context.accessPointId().orElseThrow(),
ec2DataPath(),
uid,
gid
);
} else {
builder.mountEbs(
ebsDeviceName(),
ec2DataPath(),
uid,
gid
);
}

// Reconfigure and start GitLab
builder.addCommands(
"# Reconfigure GitLab",
"gitlab-ctl reconfigure",
"echo 'GitLab configured and started' >> /var/log/userdata.log",
"",
"# Wait for GitLab to fully start",
"sleep 60",
"",
"# Get initial root password",
"if [ -f /etc/gitlab/initial_root_password ]; then",
" echo 'GitLab Root Password:' >> /var/log/userdata.log",
" cat /etc/gitlab/initial_root_password >> /var/log/userdata.log",
"fi"
);
}

@Override
public boolean supportsOidcIntegration() {
return true;
}

@Override
public OidcIntegration getOidcIntegration() {
return new GitLabOidcIntegration();
}

@Override
public String toString() {
return "GitLabApplicationSpec{" +
"applicationId='gitlab'" +
", defaultImage='gitlab/gitlab-ce:latest'" +
", applicationPort=80" +
'}';
}
}

Example 2: Grafana with OIDC

public class GrafanaApplicationSpec implements ApplicationSpec {

@Override
public String applicationId() {
return "grafana";
}

@Override
public String defaultContainerImage() {
return "grafana/grafana:latest";
}

@Override
public int applicationPort() {
return 3000;
}

@Override
public String healthCheckPath() {
return "/api/health";
}

@Override
public Map<String, String> containerEnvironmentVariables(String fqdn, boolean sslEnabled, String authMode) {
Map<String, String> environment = new HashMap<>();

// Grafana server configuration
if (fqdn != null && !fqdn.isBlank()) {
environment.put("GF_SERVER_ROOT_URL", (sslEnabled ? "https://" : "http://") + fqdn);
environment.put("GF_SERVER_DOMAIN", fqdn);
}

// OIDC configuration
if ("application-oidc".equals(authMode)) {
environment.put("GF_AUTH_GENERIC_OAUTH_ENABLED", "true");
environment.put("GF_AUTH_GENERIC_OAUTH_NAME", "IAM Identity Center");
environment.put("GF_AUTH_GENERIC_OAUTH_ALLOW_SIGN_UP", "true");
environment.put("GF_AUTH_GENERIC_OAUTH_SCOPES", "openid profile email");
}

return environment;
}

@Override
public boolean supportsOidcIntegration() {
return true;
}

@Override
public OidcIntegration getOidcIntegration() {
return new GrafanaOidcIntegration();
}

// ... rest of implementation
}

ApplicationSpec API Reference

Required Methods

MethodPurposeExample
applicationId()Unique identifier"jenkins", "gitlab", "vault"
defaultContainerImage()Docker image"jenkins/jenkins:lts"
applicationPort()HTTP port8080, 80, 3000
containerDataPath()Mount path in container"/var/jenkins_home"
efsDataPath()Path in EFS"/jenkins"
volumeName()Volume reference name"jenkinsHome"
containerUser()UID:GID"1000:1000"
efsPermissions()File permissions"750", "755"
ebsDeviceName()EC2 block device"/dev/xvdh"
ec2DataPath()EC2 mount path"/var/lib/jenkins"
ec2LogPaths()CloudWatch log paths["/var/log/app.log"]
configureUserData()EC2 installation scriptSee examples above

Optional Methods

MethodDefaultPurpose
healthCheckPath()"/"ALB health check endpoint
containerEnvironmentVariables(){}Container env vars
supportsOidcIntegration()falseWhether app supports OIDC
getOidcIntegration()nullOIDC configuration handler

UserDataBuilder API

The UserDataBuilder provides helper methods for EC2 configuration:

System Configuration

// Update OS packages
builder.addSystemUpdate();

// Add custom commands
builder.addCommands(
"# Install application",
"yum install -y myapp",
"systemctl enable myapp"
);

Storage Mounting

// Mount EFS
builder.mountEfs(
efsId,
accessPointId,
mountPath,
uid,
gid
);

// Mount EBS
builder.mountEbs(
deviceName,
mountPath,
uid,
gid
);

CloudWatch Integration

// Install and configure CloudWatch Agent
builder.installCloudWatchAgent(
logGroupName,
List.of("/var/log/app.log", "/var/log/access.log")
);

Ec2Context API

The Ec2Context provides deployment information:

// Stack information
String stackName = context.stackName();
String securityProfile = context.securityProfile();
String runtimeType = context.runtimeType();

// Network information
Optional<String> fqdn = context.fqdn();
boolean sslEnabled = context.sslEnabled();

// Storage information
boolean hasEfs = context.hasEfs();
Optional<String> efsId = context.efsId();
Optional<String> accessPointId = context.accessPointId();

Best Practices

1. ✅ DO: Support Both Runtimes

Test your application on both Fargate and EC2:

# Test Fargate deployment
cdk deploy -c runtimeType=FARGATE

# Test EC2 deployment
cdk deploy -c runtimeType=EC2

2. ✅ DO: Use Official Images

Prefer official Docker images when available:

  • jenkins/jenkins:lts
  • gitlab/gitlab-ce:latest
  • hashicorp/vault:latest
  • random-user/jenkins:custom

3. ✅ DO: Configure Health Checks

Provide application-specific health endpoints:

@Override
public String healthCheckPath() {
return "/api/health"; // Application health endpoint
}

4. ✅ DO: Log Everything

Send logs to CloudWatch for debugging:

builder.addCommands(
"echo 'Step completed' >> /var/log/userdata.log",
"mycommand >> /var/log/userdata.log 2>&1"
);

5. ❌ DON'T: Hard-Code Credentials

Never hard-code passwords or secrets:

// Bad
environment.put("DB_PASSWORD", "hardcoded");

// Good
environment.put("DB_PASSWORD_SECRET_ARN", secretArn);

6. ❌ DON'T: Assume Filesystem Paths

Check context for EFS vs EBS:

if (context.hasEfs()) {
// Use EFS mount
} else {
// Use EBS mount
}

Testing Your Application Plugin

Unit Tests

@Test
void testApplicationSpecMetadata() {
ApplicationSpec spec = new VaultApplicationSpec();

assertEquals("vault", spec.applicationId());
assertEquals("hashicorp/vault:latest", spec.defaultContainerImage());
assertEquals(8200, spec.applicationPort());
assertEquals("/vault/data", spec.containerDataPath());
}

@Test
void testContainerEnvironmentVariables() {
ApplicationSpec spec = new VaultApplicationSpec();

Map<String, String> env = spec.containerEnvironmentVariables(
"vault.example.com",
true,
"none"
);

assertTrue(env.containsKey("VAULT_API_ADDR"));
assertEquals("https://vault.example.com", env.get("VAULT_API_ADDR"));
}

Integration Tests

@Test
void testFargateDeployment() {
App app = new App();
Stack stack = new Stack(app, "VaultFargateTest");

ApplicationSpec spec = new VaultApplicationSpec();
ApplicationFactory factory = new ApplicationFactory(
stack,
"Vault",
RuntimeType.FARGATE,
spec
);

assertDoesNotThrow(() -> app.synth());
}

@Test
void testEc2Deployment() {
App app = new App();
Stack stack = new Stack(app, "VaultEc2Test");

ApplicationSpec spec = new VaultApplicationSpec();
ApplicationFactory factory = new ApplicationFactory(
stack,
"Vault",
RuntimeType.EC2,
spec
);

assertDoesNotThrow(() -> app.synth());
}

Distribution

Maven Central

<dependency>
<groupId>com.yourcompany</groupId>
<artifactId>vault-application</artifactId>
<version>1.0.0</version>
</dependency>

GitHub Releases

# Download JAR from your plugin repository
wget https://github.com/yourcompany/vault-application/releases/download/v1.0.0/vault-application-1.0.0.jar

# Add to classpath
java -cp "cloudforge-api.jar:vault-application-1.0.0.jar" ...

Example Application Plugins

Here are application ideas you can implement:

CI/CD Tools

  • ✅ Jenkins (built-in)
  • 🚧 GitLab
  • 🚧 Drone CI
  • 🚧 Gitea
  • 🚧 ArgoCD

Monitoring & Observability

  • 🚧 Grafana
  • 🚧 Prometheus
  • 🚧 Metabase
  • 🚧 Apache Superset

Databases

  • 🚧 PostgreSQL
  • 🚧 Redis
  • 🚧 MongoDB
  • 🚧 Cassandra

Artifact Registries

  • 🚧 Nexus Repository
  • 🚧 Harbor
  • 🚧 JFrog Artifactory

Secrets Management

  • 🚧 HashiCorp Vault
  • 🚧 Infisical

Collaboration

  • 🚧 Mattermost
  • 🚧 Rocket.Chat

Support and Community


Happy Application Development! 🚀