From bc315c3018322c391769bfe74592ac9fa82e1e5b Mon Sep 17 00:00:00 2001 From: Robert Grandl Date: Fri, 6 Oct 2023 09:44:25 -0700 Subject: [PATCH] Generate new deployment for each app version (#63) In the current implementation, we generate new deployments for each replica set other than the one that has the public listeners (e.g., main). When we rollout a new version of the app, we do rolling upgrade on the main group, hence we can do automated rollouts. However, this approach is not great and it doesn't make sense for someone to do this in production. For development/testing, someone can simply start a new app deployment and kill the old one. This PR changes the deployment such that: * we now generate unique deployments for all groups (including main) * we generate unique name for the listener service iff the user wants that; this allows the user to reuse the same listener across multiple app versions - makes it easy to do port-forwarding, curl the same IP, etc. * we added a "version" label to the deployments/HPAs/listeners, s.t., we can easily delete old deployments; e.g., `kubectl delete all --selector=version=foo` deletes all the k8s resources for the app version foo. --- internal/impl/kube.go | 73 ++++++++++++++-------------------------- internal/impl/kube.pb.go | 38 +++++++++++++-------- internal/impl/kube.proto | 5 +-- 3 files changed, 53 insertions(+), 63 deletions(-) diff --git a/internal/impl/kube.go b/internal/impl/kube.go index 5068b61..31cd0c0 100644 --- a/internal/impl/kube.go +++ b/internal/impl/kube.go @@ -79,6 +79,10 @@ type replicaSet struct { // ListenerOptions stores configuration options for a listener. type ListenerOptions struct { + // If specified, the listener service will have the name set to this value. + // Otherwise, we will generate a unique name for each app version. + ServiceName string `toml:"service_name"` + // Is the listener public, i.e., should it receive ingress traffic // from the public internet. If false, the listener is configured only // for cluster-internal access. @@ -168,11 +172,6 @@ type KubeConfig struct { Observability map[string]string } -// globalName returns an unique name that persists across app versions. -func (r *replicaSet) globalName() string { - return name{r.dep.App.Name, r.name}.DNSLabel() -} - // deploymentName returns a name that is version specific. func (r *replicaSet) deploymentName() string { return name{r.dep.App.Name, r.name, r.dep.Id[:8]}.DNSLabel() @@ -184,23 +183,13 @@ func (r *replicaSet) deploymentName() string { // collocated with main, and a component bar that is not collocated with main // calls foo. func (r *replicaSet) buildDeployment(cfg *KubeConfig) (*appsv1.Deployment, error) { - matchLabels := map[string]string{} + name := r.deploymentName() + matchLabels := map[string]string{"depName": name} podLabels := map[string]string{ "appName": r.dep.App.Name, - "depName": r.deploymentName(), + "depName": name, "metrics": r.dep.App.Name, // Needed by Prometheus to scrape the metrics. } - name := r.deploymentName() - if r.hasListeners() { - name = r.globalName() - - // Set the match and the pod labels, so they can be reachable across - // multiple app versions. - matchLabels["globalName"] = r.globalName() - podLabels["globalName"] = r.globalName() - } else { - matchLabels["depName"] = r.deploymentName() - } dnsPolicy := corev1.DNSClusterFirst if cfg.UseHostNetwork { dnsPolicy = corev1.DNSClusterFirstWithHostNet @@ -218,6 +207,7 @@ func (r *replicaSet) buildDeployment(cfg *KubeConfig) (*appsv1.Deployment, error ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: r.namespace, + Labels: map[string]string{"version": r.dep.Id[:8]}, }, Spec: appsv1.DeploymentSpec{ Selector: &metav1.LabelSelector{ @@ -234,13 +224,6 @@ func (r *replicaSet) buildDeployment(cfg *KubeConfig) (*appsv1.Deployment, error HostNetwork: cfg.UseHostNetwork, }, }, - Strategy: appsv1.DeploymentStrategy{ - Type: "RollingUpdate", - RollingUpdate: &appsv1.RollingUpdateDeployment{}, - }, - // Number of old ReplicaSets to retain to allow rollback. - RevisionHistoryLimit: ptrOf(int32(1)), - MinReadySeconds: int32(5), }, }, nil } @@ -251,10 +234,13 @@ func (r *replicaSet) buildDeployment(cfg *KubeConfig) (*appsv1.Deployment, error // it has to be reachable from the outside; for internal listeners, we generate // a ClusterIP service, reachable only from internal Service Weaver services. func (r *replicaSet) buildListenerService(lis *ReplicaSetConfig_Listener) (*corev1.Service, error) { - // Unique name that persists across app versions. // TODO(rgrandl): Specify whether the listener is public in the name. - globalLisName := name{r.dep.App.Name, "lis", lis.Name}.DNSLabel() - + // If the service name for the listener is not specified by the user, generate + // a deployment based service name. + lisServiceName := lis.ServiceName + if lisServiceName == "" { + lisServiceName = name{r.dep.App.Name, "lis", lis.Name, r.dep.Id[:8]}.DNSLabel() + } var serviceType string if lis.IsPublic { serviceType = "LoadBalancer" @@ -268,16 +254,17 @@ func (r *replicaSet) buildListenerService(lis *ReplicaSetConfig_Listener) (*core Kind: "Service", }, ObjectMeta: metav1.ObjectMeta{ - Name: globalLisName, + Name: lisServiceName, Namespace: r.namespace, Labels: map[string]string{ "lisName": lis.Name, + "version": r.dep.Id[:8], }, }, Spec: corev1.ServiceSpec{ Type: corev1.ServiceType(serviceType), Selector: map[string]string{ - "globalName": r.globalName(), + "depName": r.deploymentName(), }, Ports: []corev1.ServicePort{ { @@ -294,13 +281,7 @@ func (r *replicaSet) buildListenerService(lis *ReplicaSetConfig_Listener) (*core func (r *replicaSet) buildAutoscaler() (*autoscalingv2.HorizontalPodAutoscaler, error) { // Per deployment name that is app version specific. aname := name{r.dep.App.Name, "hpa", r.name, r.dep.Id[:8]}.DNSLabel() - - var depName string - if r.hasListeners() { - depName = r.globalName() - } else { - depName = r.deploymentName() - } + depName := r.deploymentName() return &autoscalingv2.HorizontalPodAutoscaler{ TypeMeta: metav1.TypeMeta{ APIVersion: "autoscaling/v2", @@ -309,6 +290,7 @@ func (r *replicaSet) buildAutoscaler() (*autoscalingv2.HorizontalPodAutoscaler, ObjectMeta: metav1.ObjectMeta{ Name: aname, Namespace: r.namespace, + Labels: map[string]string{"version": r.dep.Id[:8]}, }, Spec: autoscalingv2.HorizontalPodAutoscalerSpec{ ScaleTargetRef: autoscalingv2.CrossVersionObjectReference{ @@ -401,16 +383,6 @@ func (r *replicaSet) buildContainer() (corev1.Container, error) { }, nil } -// hasListeners returns whether a given replica set exports any listeners. -func (r *replicaSet) hasListeners() bool { - for _, listeners := range r.components { - if listeners.Listeners != nil { - return true - } - } - return false -} - // GenerateYAMLs generates Kubernetes YAML configurations for a given // application version. // @@ -443,6 +415,8 @@ func GenerateYAMLs(image string, dep *protos.Deployment, cfg *KubeConfig) error // Generate roles and role bindings. var generated []byte + header := fmt.Sprintf("# To delete this deployment run:\n# `kubectl delete all --selector=version=%s`\n\n", dep.Id[:8]) + generated = append(generated, []byte(header)...) content, err := generateRolesAndBindings(cfg.Namespace) if err != nil { return fmt.Errorf("unable to generate roles and bindings: %w", err) @@ -728,8 +702,13 @@ func readBinary(dep *protos.Deployment, cfg *KubeConfig) ([]*ReplicaSetConfig_Co port = externalPort externalPort++ } + var serviceName string + if opts := cfg.Listeners[lis]; opts != nil && opts.ServiceName != "" { + serviceName = opts.ServiceName + } c.Listeners = append(c.Listeners, &ReplicaSetConfig_Listener{ Name: lis, + ServiceName: serviceName, ExternalPort: port, IsPublic: public, }) diff --git a/internal/impl/kube.pb.go b/internal/impl/kube.pb.go index 7ee237d..5e7f9fa 100644 --- a/internal/impl/kube.pb.go +++ b/internal/impl/kube.pb.go @@ -186,8 +186,9 @@ type ReplicaSetConfig_Listener struct { unknownFields protoimpl.UnknownFields Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` - ExternalPort int32 `protobuf:"varint,2,opt,name=external_port,json=externalPort,proto3" json:"external_port,omitempty"` - IsPublic bool `protobuf:"varint,3,opt,name=is_public,json=isPublic,proto3" json:"is_public,omitempty"` + ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` + ExternalPort int32 `protobuf:"varint,3,opt,name=external_port,json=externalPort,proto3" json:"external_port,omitempty"` + IsPublic bool `protobuf:"varint,4,opt,name=is_public,json=isPublic,proto3" json:"is_public,omitempty"` } func (x *ReplicaSetConfig_Listener) Reset() { @@ -229,6 +230,13 @@ func (x *ReplicaSetConfig_Listener) GetName() string { return "" } +func (x *ReplicaSetConfig_Listener) GetServiceName() string { + if x != nil { + return x.ServiceName + } + return "" +} + func (x *ReplicaSetConfig_Listener) GetExternalPort() int32 { if x != nil { return x.ExternalPort @@ -249,7 +257,7 @@ var file_internal_impl_kube_proto_rawDesc = []byte{ 0x0a, 0x18, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x69, 0x6d, 0x70, 0x6c, 0x2f, 0x6b, 0x75, 0x62, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x04, 0x69, 0x6d, 0x70, 0x6c, 0x1a, 0x1b, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, - 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xce, 0x03, + 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xf2, 0x03, 0x0a, 0x10, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x70, 0x61, 0x63, 0x65, @@ -272,17 +280,19 @@ var file_internal_impl_kube_proto_rawDesc = []byte{ 0x09, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x69, 0x6d, 0x70, 0x6c, 0x2e, 0x52, 0x65, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x53, 0x65, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, - 0x72, 0x52, 0x09, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x1a, 0x60, 0x0a, 0x08, - 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, - 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, 0x0d, - 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x72, 0x74, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x50, 0x6f, 0x72, - 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x5f, 0x70, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x18, 0x03, - 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x42, 0x34, - 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x53, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x57, 0x65, 0x61, 0x76, 0x65, 0x72, 0x2f, 0x77, 0x65, 0x61, 0x76, 0x65, - 0x72, 0x2d, 0x6b, 0x75, 0x62, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, - 0x69, 0x6d, 0x70, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x52, 0x09, 0x6c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x73, 0x1a, 0x83, 0x01, 0x0a, + 0x08, 0x4c, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x65, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, + 0x0c, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x4e, 0x61, 0x6d, 0x65, + 0x12, 0x23, 0x0a, 0x0d, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x72, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, + 0x6c, 0x50, 0x6f, 0x72, 0x74, 0x12, 0x1b, 0x0a, 0x09, 0x69, 0x73, 0x5f, 0x70, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x18, 0x04, 0x20, 0x01, 0x28, 0x08, 0x52, 0x08, 0x69, 0x73, 0x50, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x42, 0x34, 0x5a, 0x32, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, + 0x2f, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x57, 0x65, 0x61, 0x76, 0x65, 0x72, 0x2f, 0x77, + 0x65, 0x61, 0x76, 0x65, 0x72, 0x2d, 0x6b, 0x75, 0x62, 0x65, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x2f, 0x69, 0x6d, 0x70, 0x6c, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/internal/impl/kube.proto b/internal/impl/kube.proto index 543e1fb..58bdd15 100644 --- a/internal/impl/kube.proto +++ b/internal/impl/kube.proto @@ -35,8 +35,9 @@ message ReplicaSetConfig { } message Listener { string name = 1; - int32 external_port = 2; - bool is_public = 3; + string service_name = 2; + int32 external_port = 3; + bool is_public = 4; } repeated Component components = 6; }