Thank you for all the information and guidelines. I would like to go the IAM route within the cluster, which I think will be the most practical.
If you do have time available, I will take you up on the offer for an IAM example.

I have a question regarding productionized deployments.
Can the internal IRIS web-server be used, i.e. Port 52773?
Or should there still be a web-gateway between IAM and the IRIS instance?

Regarding Kubernetes:
I would think that IAM should be the ingress, is that correct?

Thank you for the answers.

I've only used the XML format up to now, where we create builds by exporting projects to XML and then import. The OBJ Load seems like it is what I need.

Thank you again.

It is not as straightforward as a Docker's solution. Even if using a container, the code will need to be pulled from github and loaded into Cache/Ensemble/IRIS. r

In the multi-developer environment, with multiple packages in different repositories, and projects using their own repos with dependencies on the packages from other repos, it is necessary to be able to take the "raw" source from github and importing it. The only way I know of importing code, is using the XML format.

The simplest will be if Cache/Ensemble/IRIS had a tool to create an XML in the correct format from the source files.
Another more difficult way is to write code that uses the code api in Cache to load the code and then export it to XML.
This is a lot of work with possible defects that will slow the process down. The idea behind tools like github and TravisCI, is to make work less, not more.

I think in order to make CI/CD with real-world SDLC processes and source control easier to utilise, that a tool like this should come standard with the IS products. If there is already a way which I do not know of, it will be great. If not, this is quite a short-fall in the product that canv run containerised and promotes CI/CD.

Try it with between 1 and 5 on the %VID. Just as slow. It is not about the 0 result, but about the time it takes.

The count on prop1=2 is around 2.7M/

Regarding the index:
Index idx On (prop1, prop3) [ Type = bitmap ];

I can't create an index on every possible filter and order by combination. The client selects what to filter on and what to order by.

We use single field indices to cater for this, and combined field indexes for uniqueness constraints. 

Let's change the data and then test it with an order by:
Prop1 is i %Integer with valuelist of 1 to 10. Populated randomly, with 60% of the values being 2.
Prop2 is can stay as is and not relevant in this test.
Prop3 is a %String of length 20. Populate with 4M random values.
Prop1 has a bitmap index.
Prop2 has a bitmap index.
Prop3 has an index.

To use order by, it needs to look like this, and this is a lot slower:
select %ID,prop3 
from mp.test 
where %ID in 
  (SELECT * FROM 
    (SELECT TOP ALL %ID
     FROM mp.test 
     WHERE prop1 = 2
     ORDER BY prop3
  
  WHERE %VID BETWEEN 3000000 AND 30000200
)

Class mp.test Extends (%Persistent, %Populate)
{

Property prop1 As %Integer(POPSPEC = ".PopulateProp1()", VALUELIST = ",1,2,3,4,5,6,7,8,9,10");

Property prop2 As %Boolean(POPSPEC = "Integer(0,1)");

Property prop3 As %String(MAXLEN = "", POPSPEC = "LastName()");

Index prop1Index On prop1 [ Type = bitmap ];

Index prop2Index On prop2 [ Type = bitmap ];

Index prop3Index On prop3;

ClassMethod Fill(total)
{
    ;d DISABLE^%NOJRN
    ;d ..%KillExtent()
    ;do ..Populate(5000000)
}

Method PopulateProp1() As %Integer
{
    set tInt = $random(19) + 1
    set:(tInt > 10) tInt = 2
    return tInt
}

ClassMethod Query()
{
    s sql=##class(%SQL.Statement).%New()
    s query = "select %ID,prop3 from mp.test where %ID in  (SELECT * FROM  (SELECT TOP ALL %ID FROM mp.test  WHERE prop1 = 2 ORDER BY prop3) WHERE %VID BETWEEN 3000000 AND 30000005)"
    w !,"Query Before Prepare:  ",$zh
    s sc=sql.%Prepare(query)
    w !,"Query After Prepare:  ",$zh
    i $$$ISOK(sc) {
        s rset=sql.%Execute()
        w !,"Query After Execute:  ",$zh
        i 'rset.%SQLCODE {
            s t1=$zh
            d rset.%Next()
            w $zh-t1,!
            d rset.%Display()
        }
        w !,"Query All Processed:  ",$zh
    } else {
        w $System.Status.GetErrorText(sc)
    }
}

/// d ##class(mp.test).Test()
ClassMethod Test()
{
    w !,"Before fill: ",$zh
    d ..Fill()
    w !,"After fill: ",$zh
    do ..Query()
}

}

Terminal Output:

DEV>d ##class(mp.test).Test()
 
Before fill: 84807.575139
After fill: 84807.575144
Query Before Prepare:  84807.575158
Query After Prepare:  84807.666853
Query After Execute:  84807.6669688.009005
ID      prop3
 
0 Rows(s) Affected
Query All Processed:  84815.676129

Have you tried that with an order by on one of the fields?

When doing the order by, you need to select a top all on the inner query so that it can order by. 

The sorting needs to happen before the %VID filters areapplied.

Example: Filter on a property that has a bitmap index for many records.
order by another property wich is an alphanumeric value, which is not unique.

Yes. The top all kills the performance.

59.516 seconds from 7.8 seconds without the order by and top all.

I am having similar issues.
The speed is faster, but the order by is necessary.

Our table holds around 5M entries, with the select done as per above, it still takes 7 to 8 seconds to load each page, which is unacceptable for my client.

I have not looked at RESTForms, so I can only provide a biased opinion on the DynamicObject Adapter.

We have used this for REST CRUD interfaces. This enabled us to use the same message structure classes for SOAP, REST XML and REST JSON interfaces. On the REST interfaces we use the ContentType to determine whether we should use the XML Reader to parse or the DynamicObject adapter. After the parsing, we encapsulate the message instance in an Ensemble message to invoke a business service in the cases where it is not just CRUD.