Skip to content

Commit 1a12a7a

Browse files
authored
Ensure Traefik reloads TLS cert when it is changed (#270)
1 parent 0251885 commit 1a12a7a

File tree

2 files changed

+58
-74
lines changed

2 files changed

+58
-74
lines changed

cli/internal/install/cloudinstall/compute.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -633,7 +633,7 @@ func clusterNeedsUpdating(cluster, existingCluster armcontainerservice.ManagedCl
633633
return true, false
634634
}
635635

636-
if np.OSSKU != existingNp.OSSKU {
636+
if *np.OSSKU != *existingNp.OSSKU {
637637
return true, false
638638
}
639639

cli/internal/install/cloudinstall/helm.go

Lines changed: 57 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
"maps"
1111
"net/http"
1212
"os"
13-
"reflect"
1413
"sort"
1514
"strings"
1615
"time"
@@ -31,7 +30,6 @@ import (
3130
"helm.sh/helm/v3/pkg/storage/driver"
3231
batchv1 "k8s.io/api/batch/v1"
3332
corev1 "k8s.io/api/core/v1"
34-
apierrors "k8s.io/apimachinery/pkg/api/errors"
3533
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3634
"k8s.io/apimachinery/pkg/runtime"
3735
"k8s.io/apimachinery/pkg/runtime/serializer"
@@ -44,6 +42,8 @@ import (
4442
const (
4543
TraefikNamespace = "traefik"
4644
TraefikPrivateLinkServiceName = "traefik"
45+
46+
AzureLinuxImage = "mcr.microsoft.com/azurelinux/base/core:3.0"
4747
)
4848

4949
func (inst *Installer) installTraefik(ctx context.Context, restConfigPromise *install.Promise[*rest.Config], keyVaultClientManagedIdentityPromise *install.Promise[*armmsi.Identity]) (any, error) {
@@ -54,11 +54,6 @@ func (inst *Installer) installTraefik(ctx context.Context, restConfigPromise *in
5454

5555
log.Ctx(ctx).Info().Msg("Installing Traefik")
5656

57-
clientset := kubernetes.NewForConfigOrDie(restConfig)
58-
if err := inst.ensureTraefikDynamicConfigMap(ctx, clientset); err != nil {
59-
return nil, fmt.Errorf("failed to ensure Traefik dynamic ConfigMap: %w", err)
60-
}
61-
6257
var serviceAnnotations map[string]any
6358
if inst.Config.Cloud.PrivateNetworking {
6459
serviceAnnotations = map[string]any{
@@ -112,6 +107,11 @@ func (inst *Installer) installTraefik(ctx context.Context, restConfigPromise *in
112107
"additionalArguments": []string{
113108
"--entryPoints.websecure.http.tls=true",
114109
},
110+
"deployment": map[string]any{
111+
"additionalContainers": []corev1.Container{
112+
getConfigReloaderSidecar(),
113+
},
114+
},
115115
}}
116116

117117
usingCertificate := keyVaultClientManagedIdentityPromise != nil
@@ -125,30 +125,29 @@ func (inst *Installer) installTraefik(ctx context.Context, restConfigPromise *in
125125
"azure.workload.identity/client-id": *kvClientIdentity.Properties.ClientID,
126126
}
127127

128-
traefikConfig.Values["volumes"] = []any{
128+
traefikConfig.Values["deployment"].(map[string]any)["additionalVolumes"] = []any{
129129
map[string]any{
130-
"name": "traefik-dynamic",
131-
"mountPath": "/config",
132-
"type": "configMap",
130+
"name": "traefik-dynamic",
131+
"emptyDir": map[string]any{},
133132
},
134-
}
135-
136-
traefikConfig.Values["deployment"] = map[string]any{
137-
"additionalVolumes": []any{
138-
map[string]any{
139-
"name": "kv-certs",
140-
"csi": map[string]any{
141-
"driver": "secrets-store.csi.k8s.io",
142-
"readOnly": true,
143-
"volumeAttributes": map[string]any{
144-
"secretProviderClass": inst.Config.Cloud.TlsCertificate.CertificateName,
145-
},
133+
map[string]any{
134+
"name": "kv-certs",
135+
"csi": map[string]any{
136+
"driver": "secrets-store.csi.k8s.io",
137+
"readOnly": true,
138+
"volumeAttributes": map[string]any{
139+
"secretProviderClass": inst.Config.Cloud.TlsCertificate.CertificateName,
146140
},
147141
},
148142
},
149143
}
150144

151145
traefikConfig.Values["additionalVolumeMounts"] = []any{
146+
map[string]any{
147+
"name": "traefik-dynamic",
148+
"mountPath": "/config",
149+
"readOnly": true,
150+
},
152151
map[string]any{
153152
"name": "kv-certs",
154153
"mountPath": "/certs",
@@ -158,7 +157,7 @@ func (inst *Installer) installTraefik(ctx context.Context, restConfigPromise *in
158157

159158
traefikConfig.Values["additionalArguments"] = append(
160159
traefikConfig.Values["additionalArguments"].([]string),
161-
"--providers.file.filename=/config/dynamic.toml")
160+
"--providers.file.directory=/config")
162161
}
163162

164163
var overrides *HelmChartConfig
@@ -228,57 +227,42 @@ func (inst *Installer) installTraefik(ctx context.Context, restConfigPromise *in
228227
return nil, nil
229228
}
230229

231-
func (inst *Installer) ensureTraefikDynamicConfigMap(ctx context.Context, clientset *kubernetes.Clientset) error {
232-
configMapName := "traefik-dynamic"
233-
namespace := TraefikNamespace
234-
235-
desiredData := map[string]string{
236-
"dynamic.toml": `
237-
# Dynamic configuration
238-
[[tls.certificates]]
239-
certFile = "/certs/tls.crt"
240-
keyFile = "/certs/tls.key"
241-
`,
242-
}
243-
244-
// Check if the ConfigMap already exists
245-
existingConfigMap, err := clientset.CoreV1().ConfigMaps(namespace).Get(ctx, configMapName, metav1.GetOptions{})
246-
if err == nil {
247-
// ConfigMap exists, check if the data matches
248-
if reflect.DeepEqual(existingConfigMap.Data, desiredData) {
249-
// No update needed
250-
return nil
251-
}
252-
253-
// Update the ConfigMap if the data is different
254-
existingConfigMap.Data = desiredData
255-
_, err = clientset.CoreV1().ConfigMaps(namespace).Update(ctx, existingConfigMap, metav1.UpdateOptions{})
256-
if err != nil {
257-
return fmt.Errorf("failed to update ConfigMap: %w", err)
258-
}
259-
return nil
260-
}
261-
262-
if !apierrors.IsNotFound(err) {
263-
// Return any error other than "not found"
264-
return fmt.Errorf("failed to get ConfigMap: %w", err)
265-
}
266-
267-
// ConfigMap does not exist, create it
268-
newConfigMap := &corev1.ConfigMap{
269-
ObjectMeta: metav1.ObjectMeta{
270-
Name: configMapName,
271-
Namespace: namespace,
230+
// This is a sidecar for the Traefik pod that writes a "dynamic" configuration file containing TLS certificate paths.
231+
// Whenever it detects that the cert has changed, it touches the configuration file to trigger Traefik to reload it.
232+
func getConfigReloaderSidecar() corev1.Container {
233+
script := `
234+
crt_hash=$(sha256sum /certs/tls.crt 2> /dev/null)
235+
echo '{"tls": {"certificates": [{"certFile":"/certs/tls.crt","keyFile":"/certs/tls.key"}]}}' > /config/dynamic-config.yml;
236+
237+
while true; do
238+
sleep 5m
239+
new_hash=$(sha256sum /certs/tls.crt 2> /dev/null)
240+
if [ "$crt_hash" != "$new_hash" ]; then
241+
echo "Certificate changed, updating Traefik configuration"
242+
touch /config/dynamic-config.yml
243+
crt_hash=$new_hash
244+
fi
245+
done
246+
`
247+
248+
configReloaderSidecar := corev1.Container{
249+
Name: "config-reloader",
250+
Image: AzureLinuxImage,
251+
Command: []string{"bash", "-c", script},
252+
VolumeMounts: []corev1.VolumeMount{
253+
{
254+
Name: "traefik-dynamic",
255+
MountPath: "/config",
256+
ReadOnly: false,
257+
},
258+
{
259+
Name: "kv-certs",
260+
MountPath: "/certs",
261+
ReadOnly: true,
262+
},
272263
},
273-
Data: desiredData,
274-
}
275-
276-
_, err = clientset.CoreV1().ConfigMaps(namespace).Create(ctx, newConfigMap, metav1.CreateOptions{})
277-
if err != nil {
278-
return fmt.Errorf("failed to create ConfigMap: %w", err)
279264
}
280-
281-
return nil
265+
return configReloaderSidecar
282266
}
283267

284268
func (inst *Installer) installCertManager(ctx context.Context, restConfigPromise *install.Promise[*rest.Config]) (any, error) {

0 commit comments

Comments
 (0)