Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add ALB Ingress controller support #444

Merged
merged 8 commits into from
Apr 1, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions cmd/rollouts-controller/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,7 @@ import (

const (
// CLIName is the name of the CLI
cliName = "argo-rollouts"

cliName = "argo-rollouts"
defaultIstioVersion = "v1alpha3"
)

Expand All @@ -49,6 +48,8 @@ func newCommand() *cobra.Command {
serviceThreads int
ingressThreads int
istioVersion string
albIngressClasses []string
nginxIngressClasses []string
)
var command = cobra.Command{
Use: cliName,
Expand Down Expand Up @@ -121,7 +122,9 @@ func newCommand() *cobra.Command {
instanceID,
metricsPort,
k8sRequestProvider,
defaultIstioVersion)
defaultIstioVersion,
nginxIngressClasses,
albIngressClasses)

// notice that there is no need to run Start methods in a separate goroutine. (i.e. go kubeInformerFactory.Start(stopCh)
// Start method is non-blocking and runs all registered informers in a dedicated goroutine.
Expand All @@ -135,6 +138,10 @@ func newCommand() *cobra.Command {
return nil
},
}

defaultALBIngressClass := []string{"alb"}
defaultNGINXIngressClass := []string{"nginx"}

clientConfig = addKubectlFlagsToCmd(&command)
command.Flags().Int64Var(&rolloutResyncPeriod, "rollout-resync", controller.DefaultRolloutResyncPeriod, "Time period in seconds for rollouts resync.")
command.Flags().StringVar(&logLevel, "loglevel", "info", "Set the logging level. One of: debug|info|warn|error")
Expand All @@ -147,6 +154,8 @@ func newCommand() *cobra.Command {
command.Flags().IntVar(&serviceThreads, "service-threads", controller.DefaultServiceThreads, "Set the number of worker threads for the Service controller")
command.Flags().IntVar(&ingressThreads, "ingress-threads", controller.DefaultIngressThreads, "Set the number of worker threads for the Ingress controller")
command.Flags().StringVar(&istioVersion, "istio-api-version", defaultIstioVersion, "Set the default Istio apiVersion that controller should look when manipulating VirtualServices.")
command.Flags().StringArrayVar(&albIngressClasses, "alb-ingress-classes", defaultALBIngressClass, "Defines all the ingress class annotations that the alb ingress controller operates on. Defaults to alb")
command.Flags().StringArrayVar(&nginxIngressClasses, "nginx-ingress-classes", defaultNGINXIngressClass, "Defines all the ingress class annotations that the nginx ingress controller operates on. Defaults to nginx")
return &command
}

Expand Down
5 changes: 5 additions & 0 deletions controller/controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ func NewManager(
metricsPort int,
k8sRequestProvider *metrics.K8sRequestsCountProvider,
defaultIstioVersion string,
nginxIngressClasses []string,
albIngressClasses []string,
) *Manager {

utilruntime.Must(rolloutscheme.AddToScheme(scheme.Scheme))
Expand Down Expand Up @@ -192,6 +194,9 @@ func NewManager(
RolloutWorkQueue: rolloutWorkqueue,

MetricsServer: metricsServer,

ALBClasses: albIngressClasses,
NGINXClasses: nginxIngressClasses,
})

cm := &Manager{
Expand Down
72 changes: 72 additions & 0 deletions docs/features/traffic-management/alb.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# AWS Application Load Balancer (ALB)

The [AWS ALB Ingress Controller](https://kubernetes-sigs.github.io/aws-alb-ingress-controller/) enables traffic management through an Ingress object that configuring an ALB that routes traffic proportionally to different services.

The ALB consists of a listener and rules with actions. Listeners define how traffic from client comes in, and rules define how to handle those requests with various actions. One action allows users to forward traffic to multiple TargetGroups (with each being defined as a Kubernetes service) You can read more about ALB concepts [here](https://docs.aws.amazon.com/elasticloadbalancing/latest/application/introduction.html).

An ALB Ingress defines a desired ALB with listener and rules through its annotations and spec. The ALB Ingress controller honors an annotation on an Ingress called `alb.ingress.kubernetes.io/actions.<service-name>` that allows users to define the actions of a service listed in the Ingress with a "use-annotations" value for the ports. Below is an example of an ingress:

```yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
annotations:
alb.ingress.kubernetes.io/actions.stable-service: |
{
"Type":"forward",
"ForwardConfig":{
"TargetGroups":[
{
"Weight":10,
"ServiceName":"canary-service",
"ServicePort":"80"
},
{
"Weight":90,
"ServiceName":"stable-service",
"ServicePort":"80"
}
]
}
}
kubernetes.io/ingress.class: alb
name: ingress
spec:
rules:
- http:
paths:
- backend:
serviceName: stable-service
servicePort: use-annotation
path: /*
```
This Ingress uses the `alb.ingress.kubernetes.io/actions.stable-service` annotation to define how to route traffic to the various services for the rule with the `stable-service` serviceName instead of sending traffic to the canary-service service. You can read more about these annotations on the official [documentation](https://kubernetes-sigs.github.io/aws-alb-ingress-controller/guide/ingress/annotation/#actions).

## Integration with Argo Rollouts
There are a couple of required fields in a Rollout to send split traffic between versions using ALB ingresses. Below is an example of a Rollout with those fields:

```yaml
apiVersion: argoproj.io/v1alpha1
kind: Rollout
spec:
...
strategy:
canary:
canaryService: canary-service # required
stableService: stable-service # required
trafficRouting:
alb:
ingress: ingress # required
annotationPrefix: custom.alb.ingress.kubernetes.io # optional
```

The ingress field is a reference to an Ingress in the same namespace of the Rollout. The Rollout requires this Ingress to modify the ALB to route traffic to the stable and canary Services. Within the Ingress, looks for the stableService within the Ingress's rules and adds an action annotation for that the action. As the Rollout progresses through the Canary steps, the controller updates the Ingress's action annotation to reflect the desired state of the Rollout enabling traffic splitting between two different versions.

Since the ALB Ingress controller allows users to configure the annotation prefix used by the Ingress controller, Rollouts can specify the optional `annotationPrefix` field. The Ingress uses that prefix instead of the default `alb.ingress.kubernetes.io` if the field set.

The Rollout adds another annotation called `rollouts.argoproj.io/managed-alb-actions` to the Ingress to help the controller manage the Ingresses. This annotation indicates which actions are being managed by Rollout objects (since multiple Rollouts can reference one Ingress). If a Rollout is deleted, the Argo Rollouts controller uses this annotation to see that this action is no longer managed, and it is reset to only the stable service with 100 weight.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a Rollout is deleted, the Argo Rollouts controller uses this annotation to see that this action is no longer managed, and it is reset to only the stable service with 100 weight.

I wanted to call out that this behavior is such that we would be leaving around our annotation when the original ingress object did not have it in the beginning.

I think this is the correct behavior, since it is safer than deleting the annotation, which could cause downtime, but just wanted to point out that we could be leaving around leftover cruft.


## Using Argo Rollouts with multiple ALB ingress controllers
As a default, the Argo Rollouts controller only operates on ingresses with the `kubernetes.io/ingress.class` annotation set to `alb`. A user can configure the controller to operate on Ingresses with different `kubernetes.io/ingress.class` values by specifying the `--alb-ingress-classes` flag. A user can list the `--alb-ingress-classes` flag multiple times if the Argo Rollouts controller should operate on multiple values. This solves the case where a cluster has multiple Ingress controllers operating on different `kubernetes.io/ingress.class` values.

If the user would like the controller to operate on any Ingress without the `kubernetes.io/ingress.class` annotation, a user should add the following `--alb-ingress-classes ''`.
1 change: 1 addition & 0 deletions docs/features/traffic-management/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Argo Rollouts enables traffic management by manipulating the Service Mesh resour

- [Istio](istio.md)
- [Nginx Ingress Controller](nginx.md)
- [AWS ALB Ingress Controller](alb.md)
- File a ticket [here](https://github.com/argoproj/argo-rollouts/issues) if you would like another implementation (or thumbs up it if that issue already exists)

Regardless of the Service Mesh used, the Rollout object has to set a canary Service and a stable Service in its spec. Here is an example with those fields set:
Expand Down
8 changes: 7 additions & 1 deletion docs/features/traffic-management/nginx.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,10 @@ The stable Ingress field is a reference to an Ingress in the same namespace of t

The controller routes traffic to the canary Service by creating a second Ingress with the canary annotations. As the Rollout progresses through the Canary steps, the controller updates the canary Ingress's canary annotations to reflect the desired state of the Rollout enabling traffic splitting between two different versions.

Since the Nginx Ingress controller allows users to configure the annotation prefix used by the Ingress controller, Rollouts can specify the optional `annonationPrefix` field. The canary Ingress uses that prefix instead of the default `nginx.ingress.kubernetes.io` if the field set.
Since the Nginx Ingress controller allows users to configure the annotation prefix used by the Ingress controller, Rollouts can specify the optional `annotationPrefix` field. The canary Ingress uses that prefix instead of the default `nginx.ingress.kubernetes.io` if the field set.


## Using Argo Rollouts with multiple NGINX ingress controllers
As a default, the Argo Rollouts controller only operates on ingresses with the `kubernetes.io/ingress.class` annotation set to `nginx`. A user can configure the controller to operate on Ingresses with different `kubernetes.io/ingress.class` values by specifying the `--nginx-ingress-classes` flag. A user can list the `--nginx-ingress-classes` flag multiple times if the Argo Rollouts controller should operate on multiple values. This solves the case where a cluster has multiple Ingress controllers operating on different `kubernetes.io/ingress.class` values.

If the user would like the controller to operate on any Ingress without the `kubernetes.io/ingress.class` annotation, a user should add the following `--nginx-ingress-classes ''`.
97 changes: 97 additions & 0 deletions ingress/alb.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package ingress

import (
"encoding/json"
"fmt"
"strings"

"github.com/sirupsen/logrus"

extensionsv1beta1 "k8s.io/api/extensions/v1beta1"

"github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
ingressutil "github.com/argoproj/argo-rollouts/utils/ingress"
jsonutil "github.com/argoproj/argo-rollouts/utils/json"
logutil "github.com/argoproj/argo-rollouts/utils/log"
)

func (c *Controller) syncALBIngress(ingress *extensionsv1beta1.Ingress, rollouts []*v1alpha1.Rollout) error {

managedActions, err := ingressutil.NewManagedALBActions(ingress.Annotations[ingressutil.ManagedActionsAnnotation])
if err != nil {
return nil
}
actionHasExistingRollout := map[string]bool{}
for i := range rollouts {
rollout := rollouts[i]
if _, ok := managedActions[rollout.Name]; ok {
actionHasExistingRollout[rollout.Name] = true
c.enqueueRollout(rollout)
}
}
newIngress := ingress.DeepCopy()
modified := false
for roName := range managedActions {
if _, ok := actionHasExistingRollout[roName]; !ok {
modified = true
actionKey := managedActions[roName]
delete(managedActions, roName)
resetALBAction, err := getResetALBActionStr(ingress, actionKey)
if err != nil {
logrus.WithField(logutil.IngressKey, ingress.Name).WithField(logutil.NamespaceKey, ingress.Namespace).Error(err)
return nil
}
newIngress.Annotations[actionKey] = resetALBAction
}
}
if !modified {
return nil
}
newManagedStr := managedActions.String()
newIngress.Annotations[ingressutil.ManagedActionsAnnotation] = newManagedStr
if newManagedStr == "" {
delete(newIngress.Annotations, ingressutil.ManagedActionsAnnotation)
}
_, err = c.client.ExtensionsV1beta1().Ingresses(ingress.Namespace).Update(newIngress)
return err
}

func getResetALBActionStr(ingress *extensionsv1beta1.Ingress, action string) (string, error) {
parts := strings.Split(action, ingressutil.ALBActionPrefix)
if len(parts) != 2 {
return "", fmt.Errorf("unable to parse action to get the service %s", action)
}
service := parts[1]

previousActionStr := ingress.Annotations[action]
var previousAction ingressutil.ALBAction
err := json.Unmarshal([]byte(previousActionStr), &previousAction)
if err != nil {
return "", fmt.Errorf("unable to unmarshal previous ALB action")
}

var port string
for _, tg := range previousAction.ForwardConfig.TargetGroups {
if tg.ServiceName == service {
port = tg.ServicePort
}
}
if port == "" {
return "", fmt.Errorf("unable to reset annotation due to missing port")
}

albAction := ingressutil.ALBAction{
Type: "forward",
ForwardConfig: ingressutil.ALBForwardConfig{
TargetGroups: []ingressutil.ALBTargetGroup{
{
ServiceName: service,
ServicePort: port,
Weight: int64(100),
},
},
},
}
bytes := jsonutil.MustMarshal(albAction)
return string(bytes), nil
}
Loading