This section focuses on the Cellery syntax and explain how to compose cells.
- To get more information on the concepts and specification, please check Cellery specification.
The Cellery Language is a subset of Ballerina and Ballerina extensions. Therefore the language syntax of Cellery resembles with normal Ballerina language syntax.
This README explains,
- Cell
- Composite
- Component
- Ingresses
- Environmental Variables
- Resources
- Scaling
- Probes
- Volumes
- Intra-cell Communication
- Inter-cell Communication
A Cell is a collection of components, APIs, Ingresses and Policies. A Cell record is initialized as follows; Components can be added to the components map in the CellImage record.
cellery:CellImage helloCell = {
components: {
helloComp:helloWorldComp
}
};
A composite is a cut down version of cells. A composite doesn't have a network boundary. A gateway is not getting attached to a composite and therefore a composite components support only following ingress types.
Components matching above criteria can be added to the components map in the Composite record.
cellery:Composite helloComposite = {
components: {
helloComp:helloWorldComp
}
};
A component represents an implementation of the business logic (in a docker image or a legacy system) and a collections of network accessible entry points (Ingresses) and parameters. A sample component with inline definitions would be as follows:
cellery:Component helloComponent = {
name: "hello-api",
src: {
image: "docker.io/wso2cellery/samples-hello-world-api" // source docker image
},
ingresses: {
helloApi: <cellery:HttpApiIngress>{
port: 9090,
context: "hello",
definition: {
resources: [
{
path: "/",
method: "GET"
}
]
},
expose: "global"
}
},
envVars: {
MESSAGE: { value: "hello" }
}
};
An Ingress represents an entry point into a cell component. An ingress can be an HTTP, TCP, GRPC or Web endpoint.
HttpApiIngress
supports defining HTTP API as an entry point for a cell. API definitions can be provided inline or as a swagger file.
A sample HttpApiIngress
instance with inline API definition would be as follows:
cellery:HttpApiIngress helloAPI = {
port: 9090,
context: "hello",
apiVersion: "1.0.0",
definition: {
resources: [
{
path: "/",
method: "GET"
}
]
},
expose: "global"
};
A sample HttpApiIngress
record with swagger 2.0 definition can be defined as follows. The definitions are resolved at the build time.
Therefore the build method is implemented to parse swagger file, and assign to ingress.
cellery:HttpApiIngress employeeIngress = {
port: 8080,
context: "employee",
expose: "local"
};
public function build(cellery:ImageName iName) returns error? {
cellery:HttpApiIngress helloAPI = {
port: 9090,
context: "hello",
apiVersion: "1.0.0",
definition: <cellery:ApiDefinition>cellery:readSwaggerFile("./resources/employee.swagger.json"),
expose: "global"
};
...
}
A particular HttpApiIngress
can be exposed as an API by setting expose
field. This field accepts two values.
- `local`: Expose an HTTP API via local cell gateway.
- `global`: Expose an HTTP API via global gateway.
By setting expose
to global
, the API will be published to the global gateway ready to receive external traffic.
In this case, the cell name and the context provided in the ingress is combined to create the full context for the
global API.
cellery:HttpApiIngress helloAPI = {
port: 9090,
context: "hello",
apiVersion: "1.0.0",
// ...
expose: "global"
};
In the above ingress exposed as global, the API will he published with the context /<cell-name>/hello/1.0.0
.
In addition, user can expose the ingresses of a particular cell to outside with a common root context by using the
globalPublisher
configuration.
cellery:HttpApiIngress helloAPI = {
context: "hello",
apiVersion: "1.0.0",
// ...
expose: "global"
};
cellery:CellImage hrCell = {
globalPublisher: {
apiVersion: "1.0.1",
context: "myorg"
},
};
Here, http ingresses marked as expose: "global"
will be published to the global API gateway. The context of the each
published API will be the concatenation of context defined in globalPublisher
and the sub context defined in each ingress.
Therefore, the global API will be published as /myorg/hello/1.0.1
. Note that the root context /myorg
is picked from the
globalPublisher
section and the sub context hello
is picked from the http ingress. The version is specified in globalPublisher
section.
Web cell ingress allows web traffic to the cell. A sample Web ingress would be as following: Web ingress are always exposed globally.
cellery:WebIngress webIngress = {
port: 8080,
gatewayConfig: {
vhost: "abc.com",
context: "/demo" //default to “/”
}
};
TLS can be defined to a web ingress as below:
// Web Component
cellery:Component webComponent = {
name: "web-ui",
src: {
image: "wso2cellery/samples-hello-world-webapp"
},
ingresses: {
webUI: <cellery:WebIngress>{ // Ingress is defined in line in the ingresses map.
port: 80,
gatewayConfig: {
vhost: "hello.com",
tls: {
key: "",
cert: ""
}
}
}
}
};
// Create the cell image with cell web component.
cellery:CellImage webCell = {
components: {
webComp: webComponent
}
};
Values for tls key and tls cert can be assigned at the run method as below.
public function run(cellery:ImageName iName, map<cellery:ImageName> instances, boolean startDependencies, boolean shareDependencies) returns (cellery:InstanceState[] | error?) {
//Read TLS key file path from ENV and get the value
string tlsKey = readFile(config:getAsString("tls.key"));
string tlsCert = readFile(config:getAsString("tls.cert"));
//Assign values to cell->component->ingress
cellery:CellImage webCell = check cellery:constructCellImage(iName);
cellery:WebIngress webUI = <cellery:WebIngress>webCell.components["webComp"]["ingresses"]["webUI"];
webUI["gatewayConfig"]["tls"]["key"] = tlsKey;
webUI["gatewayConfig"]["tls"]["cert"] = tlsCert;
// Create the cell instance
return <@untainted> cellery:createInstance(webCell, iName);
}
// Read the file given in filePath and return the content as a string.
function readFile(string filePath) returns (string) {
io:ReadableByteChannel bchannel = io:openReadableFile(filePath);
io:ReadableCharacterChannel cChannel = new io:ReadableCharacterChannel(bchannel, "UTF-8");
var readOutput = cChannel.read(2000);
if (readOutput is string) {
return readOutput;
} else {
error err = error("Unable to read file " + filePath);
panic err;
}
}
Web ingress support Open ID connect. OIDC config can be defined as below.
cellery:Component portalComponent = {
name: "portal",
src: {
image: "wso2cellery/samples-pet-store-portal"
},
ingresses: {
portal: <cellery:WebIngress>{ // Web ingress will be always exposed globally.
port: 80,
gatewayConfig: {
vhost: "pet-store.com",
context: "/",
oidc: {
nonSecurePaths: ["/", "/app/*"],
providerUrl: "",
clientId: "",
clientSecret: "",
redirectUrl: "http://pet-store.com/_auth/callback",
baseUrl: "http://pet-store.com/",
subjectClaim: "given_name"
}
}
}
}
};
If dynamic client registration is used dcr configs can be provided as below in the clientSecret
field.
ingresses: {
portal: <cellery:WebIngress>{
port: 80,
gatewayConfig: {
vhost: "pet-store.com",
context: "/portal",
oidc: {
nonSecurePaths: ["/portal"], // Default [], optional field
providerUrl: "https://idp.cellery-system/oauth2/token",
clientId: "petstoreapplicationcelleryizza",
clientSecret: {
dcrUser: "admin",
dcrPassword: "admin"
},
redirectUrl: "http://pet-store.com/_auth/callback",
baseUrl: "http://pet-store.com/items/",
subjectClaim: "given_name"
}
}
}
},
Similar to above sample the clientSecret
and clientId
values be set at the run method to pass value at the run time without burning to the image.
TCP ingress supports defining TCP endpoints. A sample TCP ingress would be as following:
cellery:TCPIngress tcpIngress = {
backendPort: 3306,
gatewayPort: 31406
};
The backendPort is the actual container port which is exposed by the container. The gatewayPort is the port exposed by the cell gateway.
GRPC ingress supports defining GRPC endpoints. This is similar to TCP ingress with optional field to define protofile. protofile field is resolved at build method since protofile is packed at build time.
cellery:GRPCIngress grpcIngress = {
backendPort: 3306,
gatewayPort: 31406
};
public function build(cellery:ImageName iName) returns error? {
grpcIngress.protoFile = "./resources/employee.proto";
...
}
HttpPort ingress supports defining a http port as an endpoint. This ingress is used to declare endpoint in composite components.
//Stock Component
cellery:Component stockComponent = {
name: "stock",
src: {
image: "wso2cellery/sampleapp-stock:0.3.0"
},
ingresses: {
http:<cellery:HttpsPortIngress>{port: 8080}
}
};
cellery:Composite stockComposite = {
components: {
stockComp: stockComponent
}
};
A cell developer can require a set of environment parameters that should be passed to a Cell instance for it to be properly functional.
// Employee Component
cellery:Component employeeComponent = {
name: "employee",
src: {
image: "docker.io/celleryio/sampleapp-employee"
},
ingresses: {
employee: <cellery:HttpApiIngress>{
port: 8080,
context: "employee",
expose: "local"
}
},
envVars: {
SALARY_HOST: { value: "" }
},
labels: {
team: "HR"
}
};
Note the parameters SALARY_HOST in the Cell definition above. This parameters can be set in the run time of this Cell.
public function run(cellery:ImageName iName, map<cellery:ImageName> instances, boolean startDependencies, boolean shareDependencies) returns (cellery:InstanceState[] | error?) {
employeeCell.components.empComp.envVars.SALARY_HOST.value = config:getAsString("salary.host");
return <@untainted> cellery:createInstance(employeeCell, iName);
}
Sometimes you may need to pass one component's host and port as an environment variable to another component for intra-cell
communication. For this, you can use cellery:getHost(<component>)
and cellery:getPort(<component>)
functions.
cellery:Component myComponent = {
name: "comp1",
...
envVars: {
COMP2_ADDRESS: {
value: cellery:getHost(comp2) + ":" + cellery:getPort(comp2)
},
},
...
};
Cellery allows to specify how much CPU and memory (RAM) each component needs. Resources can be specified as requests and limits. When component have resource requests specified, the scheduler can make better decisions about which nodes to place component on. When component have their limits specified, contention for resources on a node can be handled in a specified manner.
Following is sample syntax on how to define limits/requests.
import celleryio/cellery;
public function build(cellery:ImageName iName) returns error? {
//Stock Component
cellery:Component stockComponent = {
name: "stock",
src: {
image: "wso2cellery/sampleapp-stock:0.3.0"
},
ingresses: {
stock: <cellery:HttpApiIngress>{ port: 8080,
context: "stock",
definition: {
resources: [
{
path: "/options",
method: "GET"
}
]
},
expose: "local"
}
},
resources: {
requests: {
memory: "64Mi",
cpu: "250m"
},
limits: {
memory: "128Mi",
cpu: "500m"
}
}
};
cellery:CellImage stockCell = {
components: {
stockComp: stockComponent
}
};
return <@untainted> cellery:createImage(stockCell, iName);
}
Autoscale policies can be specified by the Cell developer at Cell creation time.
If a component has a scaling policy as the AutoScalingPolicy
then component will be scaled with horizontal pod autoscaler.
import ballerina/io;
import celleryio/cellery;
public function build(cellery:ImageName iName) returns error? {
//Pet Component
cellery:Component petComponent = {
name: "pet-service",
src: {
image: "docker.io/isurulucky/pet-service"
},
ingresses: {
stock: <cellery:HttpApiIngress>{ port: 9090,
context: "petsvc",
definition: {
resources: [
{
path: "/*",
method: "GET"
}
]
}
}
},
scalingPolicy: <cellery:AutoScalingPolicy> {
minReplicas: 1,
maxReplicas: 10,
metrics: {
cpu: <cellery:Value>{ threshold : "500m" },
memory: <cellery:Percentage> { threshold : 50 }
}
}
};
cellery:CellImage petCell = {
components: {
petComp: petComponent
}
};
return <@untainted> cellery:createImage(petCell, iName);
}
The Auto-scale policy defined by the developer can be overridden at the runtime by providing a different policy at the runtime.
Zero scaling is powered by Knative. The zero-scaling have minimum replica count 0, and hence when the component did not get any request, the component will be terminated and it will be running back once a request was directed to the component.
A zero-scaling policy can be defined as below.
import ballerina/io;
import celleryio/cellery;
public function build(cellery:ImageName iName) returns error? {
//Pet Component
cellery:Component petComponent = {
name: "pet-service",
src: {
image: "docker.io/isurulucky/pet-service"
},
ingresses: {
stock: <cellery:HttpApiIngress>{ port: 9090,
context: "petsvc",
definition: {
resources: [
{
path: "/*",
method: "GET"
}
]
}
}
},
scalingPolicy: <cellery:ZeroScalingPolicy> {
maxReplicas: 10,
concurrencyTarget: 25
}
};
cellery:CellImage petCell = {
components: {
petComp: petComponent
}
};
return <@untainted> cellery:createImage(petCell, iName);
}
Liveness and readiness probes can be defined for a component. The probes can be defined by the means of tcp socket, command or as a http-get.
import celleryio/cellery;
public function build(cellery:ImageName iName) returns error? {
int salaryContainerPort = 8080;
// Salary Component
cellery:Component salaryComponent = {
name: "salary",
src: {
image: "docker.io/celleryio/sampleapp-salary"
},
ingresses: {
SalaryAPI: <cellery:HttpApiIngress>{
port:salaryContainerPort,
context: "payroll",
definition: {
resources: [
{
path: "salary",
method: "GET"
}
]
},
expose: "global"
}
},
probes: {
liveness: {
initialDelaySeconds: 30,
kind: <cellery:TcpSocket>{
port:salaryContainerPort
}
}
}
};
}
import celleryio/cellery;
public function build(cellery:ImageName iName) returns error? {
int salaryContainerPort = 8080;
// Salary Component
cellery:Component salaryComponent = {
name: "salary",
src: {
image: "docker.io/celleryio/sampleapp-salary"
},
ingresses: {
SalaryAPI: <cellery:HttpApiIngress>{
port:salaryContainerPort,
context: "payroll",
definition: {
resources: [
{
path: "salary",
method: "GET"
}
]
},
expose: "global"
}
},
probes: {
readiness: {
initialDelaySeconds: 10,
timeoutSeconds: 50,
kind: <cellery:Exec>{
commands: ["bash", "-version"]
}
}
}
};
}
import celleryio/cellery;
public function build(cellery:ImageName iName) returns error? {
int salaryContainerPort = 8080;
// Salary Component
cellery:Component salaryComponent = {
name: "salary",
src: {
image: "docker.io/celleryio/sampleapp-salary"
},
ingresses: {
SalaryAPI: <cellery:HttpApiIngress>{
port:salaryContainerPort,
context: "payroll",
definition: {
resources: [
{
path: "salary",
method: "GET"
}
]
},
expose: "global"
}
},
probes: {
readiness: {
initialDelaySeconds: 10,
timeoutSeconds: 50,
kind: <cellery:HttpGet>{
port: salaryContainerPort,
path: "/healthz"
}
}
}
};
}
Volumes can be mounted to a component. Cellery support mounting 3 types of volumes to a component.
Configurations allow to decouple configuration artifacts from image content to keep containerized applications portable. Configurations can be add to component as following.
cellery:Component helloComponent = {
name: "hello-api",
src: {
image: "docker.io/wso2cellery/samples-hello-world-api-hello-service"
},
ingresses: {
helloApi: <cellery:HttpApiIngress>{ port: 9090,
context: "hello",
authenticate: false,
apiVersion: "v1.0.0",
definition: {
resources: [
{
path: "/",
method: "GET"
}
]
},
expose: "global"
}
},
volumes: {
// Mounting a non existing configuration.
// Configuration will be created and mounted to component
config: {
path: "/tmp/config",
readOnly: false,
volume:<cellery:NonSharedConfiguration>{
name:"my-config",
data:{
debug:"enable",
logs:"enable"
}
}
},
// Mounting an existing configuration.
// Configuration is already available in runtime and will be mounted to the component.
configShared: {
path: "/tmp/shared/config",
readOnly: false,
volume:<cellery:SharedConfiguration>{
name:"my-config-shared"
}
}
}
};
Secret recors let you store and manage sensitive information, such as passwords, OAuth tokens, and ssh keys. Secret can be add to component as following.
cellery:Component helloComponent = {
name: "hello-api",
src: {
image: "docker.io/wso2cellery/samples-hello-world-api-hello-service"
},
ingresses: {
helloApi: <cellery:HttpApiIngress>{ port: 9090,
context: "hello",
authenticate: false,
apiVersion: "v1.0.0",
definition: {
resources: [
{
path: "/",
method: "GET"
}
]
},
expose: "global"
}
},
volumes: {
// Mounting a non existing secret.
// Secret will be created and mounted to component
secret: {
path: "/tmp/secret/",
readOnly: false,
volume:<cellery:NonSharedSecret>{
name:cellery:generateVolumeName("my-secret"),
data:{
username:"admin",
password:"admin"
}
}
},
// Mounting an existing secret.
// Secret is already available in runtime and will be mounted to the component.
secertShared: {
path: "/tmp/shared/secret",
readOnly: false,
volume:<cellery:SharedSecret>{
name:"my-secret-shared"
}
}
}
};
Volume Claims (or PVCs) are objects that request storage resources from your cluster. They’re similar to a voucher that your deployment can redeem for storage access. Volume Claims can be add to component as following.
cellery:Component helloComponent = {
name: "hello-api",
src: {
image: "docker.io/wso2cellery/samples-hello-world-api-hello-service"
},
ingresses: {
helloApi: <cellery:HttpApiIngress>{ port: 9090,
context: "hello",
authenticate: false,
apiVersion: "v1.0.0",
definition: {
resources: [
{
path: "/",
method: "GET"
}
]
},
expose: "global"
}
},
volumes: {
// Mounting a non existing volume claim.
// Volume claim will be created and mounted to component
volumeClaim: {
path: "/tmp/pvc/",
readOnly: false,
volume:<cellery:K8sNonSharedPersistence>{
name:"pv1",
mode:"Filesystem",
storageClass:"slow",
accessMode: ["ReadWriteMany"],
request:"2G",
lookup: {
labels: {
release: "stable"
},
expressions: [{ key: "environment", operator: "In", values: ["dev", "staging"]}]
}
}
},
// Mounting an existing volume claim.
// Volume claim is already available in runtime and will be mounted to the component.
volumeClaimShared: {
path: "/tmp/pvc/shared",
readOnly: true,
volume:<cellery:K8sSharedPersistence>{
name:"pv2"
}
}
}
};
Cell components can communicate with each other. This is achieved via environment variables. Two components can be linked via
environment variables in the run method. For an example, consider the scenario below. The employee component expects
SALARY_HOST
, which is the hostname of salary component.
Employee component:
import ballerina/io;
import celleryio/cellery;
public function build(cellery:ImageName iName) returns error? {
int salaryContainerPort = 8080;
// Salary Component
cellery:Component salaryComponent = {
name: "salary",
src: {
image: "docker.io/celleryio/sampleapp-salary"
},
ingresses: {
SalaryAPI: <cellery:HttpApiIngress>{
port:salaryContainerPort,
context: "payroll",
definition: {
resources: [
{
path: "salary",
method: "GET"
}
]
},
expose: "local"
}
},
labels: {
team: "Finance",
owner: "Alice"
}
};
// Employee Component
cellery:Component employeeComponent = {
name: "employee",
src: {
image: "docker.io/celleryio/sampleapp-employee"
},
ingresses: {
employee: <cellery:HttpApiIngress>{
port: 8080,
context: "employee",
expose: "local",
definition: <cellery:ApiDefinition>cellery:readSwaggerFile("./resources/employee.swagger.json")
}
},
envVars: {
SALARY_HOST: {
value: cellery:getHost(salaryComponent)
},
PORT: {
value: salaryContainerPort
}
},
labels: {
team: "HR"
}
};
cellery:CellImage employeeCell = {
components: {
empComp: employeeComponent,
salaryComp: salaryComponent
}
};
return <@untainted> cellery:createImage(employeeCell, iName);
}
The envVar SALARY_HOST
value is provided at the build time as shown above, which enables the employee component to
communicate with the salary component.
In addition to components within a cell, Cells themselves can communicate with each other. This is also achieved via envVars. Two cells can be linked via envVar in the run method.
When a Cell Image is built it will generate a reference file describing the APIs/Ports that are exposed by itself. This cell reference will be available locally, either when you build or pull the image. This reference can be imported in another Cell definition which is depending on the former, and can be used to link the two Cells at the runtime.
Consider following cell definition:
import ballerina/io;
import celleryio/cellery;
public function build(cellery:ImageName iName) returns error? {
//Build Stock Cell
io:println("Building Stock Cell ...");
//Stock Component
cellery:Component stockComponent = {
name: "stock",
src: {
image: "docker.io/celleryio/sampleapp-stock"
},
ingresses: {
stock: <cellery:HttpApiIngress>{ port: 8080,
context: "stock",
definition: {
resources: [
{
path: "/options",
method: "GET"
}
]
},
expose: "local"
}
}
};
cellery:CellImage stockCell = {
components: {
stockComp: stockComponent
}
};
return <@untainted> cellery:createImage(stockCell, iName);
}
Generated reference file for above cell definition is as follows:
{
"stock_api_url":"http://{{instance_name}}--gateway-service:80/stock"
}
If a cell component wants to access the stock api, it can be done as below:
public function build(cellery:ImageName iName) returns error? {
//HR component
cellery:Component hrComponent = {
name: "hr",
src: {
image: "docker.io/celleryio/sampleapp-hr"
},
ingresses: {
"hr": <cellery:HttpApiIngress>{
port: 8080,
context: "hr-api",
definition: {
resources: [
{
path: "/",
method: "GET"
}
]
},
expose: "global"
}
},
dependencies: {
cells: {
stockCellDep: <cellery:ImageName>{ org: "myorg", name: "stock", ver: "1.0.0" } // dependency as a struct
}
}
};
hrComponent["envVars"] = {
stock_api_url: { value: <string>cellery:getReference(hrComponent, "stockCellDep")["stock_api_url"] }
};
// Cell Initialization
cellery:CellImage hrCell = {
components: {
hrComp: hrComponent
}
};
return <@untainted> cellery:createImage(hrCell, iName);
}
public function run(cellery:ImageName iName, map<cellery:ImageName> instances, boolean startDependencies, boolean shareDependencies) returns (cellery:InstanceState[] | error?) {
cellery:CellImage hrCell = check cellery:constructCellImage(iName);
return <@untainted> cellery:createInstance(hrCell, iName, instances);
}
The hrComponent
depends on the stockCell that is defined earlier. The dependency information are specified as a component attribute.
dependencies: {
cells: {
stockCellDep: <cellery:ImageName>{ org: "myorg", name: "stock", ver: "1.0.0" } // dependency as a struct
}
}
cellery:getReference(hrComponent, "stockCellDep").stock_api_url
method reads the json file and set value with place holder.
Note the run
method above, which takes a variable argument map for the references of the dependency cells.
These are names of already deployed cell instances, which will be used to resolve the urls and link with this
cell instance. As an example, if the stock cell instance name is stock-app
then the stockRef.stock_api_url
returns the host name of the running stock cell instance as http://stock-app--gateway-service:80/stock
.
- Developing a Cell - step by step explanation on how you could define your own cells.
- Samples - a collection of useful samples.