Skip to content
This repository was archived by the owner on Jul 30, 2021. It is now read-only.

Commit 56fcbc0

Browse files
committed
Add additional support for external etcd
Supporting external etcd should be as easy as possible. It was very difficult before this patch to support external etcd. Signed-off-by: Chuck Ha <[email protected]>
1 parent 54a04d8 commit 56fcbc0

File tree

8 files changed

+245
-71
lines changed

8 files changed

+245
-71
lines changed

cloudinit/cloudinit_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func TestNewInitControlPlaneAdditionalFileEncodings(t *testing.T) {
4646
Users: nil,
4747
NTP: nil,
4848
},
49-
Certificates: cluster.NewCertificates(),
49+
Certificates: cluster.Certificates{},
5050
ClusterConfiguration: "my-cluster-config",
5151
InitConfiguration: "my-init-config",
5252
}

cloudinit/controlplane_init.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,10 +52,6 @@ type ControlPlaneInput struct {
5252
// NewInitControlPlane returns the user data string to be used on a controlplane instance.
5353
func NewInitControlPlane(input *ControlPlaneInput) ([]byte, error) {
5454
input.Header = cloudConfigHeader
55-
if err := input.Certificates.EnsureAllExist(); err != nil {
56-
return nil, err
57-
}
58-
5955
input.WriteFiles = input.Certificates.AsFiles()
6056
input.WriteFiles = append(input.WriteFiles, input.AdditionalFiles...)
6157
userData, err := generate("InitControlplane", controlPlaneCloudInit, input)

cloudinit/controlplane_join.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,7 @@ type ControlPlaneJoinInput struct {
5050
// NewJoinControlPlane returns the user data string to be used on a new control plane instance.
5151
func NewJoinControlPlane(input *ControlPlaneJoinInput) ([]byte, error) {
5252
input.Header = cloudConfigHeader
53-
if err := input.Certificates.EnsureAllExist(); err != nil {
54-
return nil, err
55-
}
56-
53+
// TODO: Consider validating that the correct certificates exist. It is different for external/stacked etcd
5754
input.WriteFiles = input.Certificates.AsFiles()
5855
input.WriteFiles = append(input.WriteFiles, input.AdditionalFiles...)
5956
userData, err := generate("JoinControlplane", controlPlaneJoinCloudInit, input)

controllers/kubeadmconfig_controller.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
227227
return ctrl.Result{}, err
228228
}
229229

230-
certificates := internalcluster.NewCertificates()
230+
certificates := internalcluster.NewCertificatesForControlPlane(config.Spec.ClusterConfiguration)
231231
if err := certificates.LookupOrGenerate(ctx, r.Client, cluster, config); err != nil {
232232
log.Error(err, "unable to lookup or create cluster certificates")
233233
return ctrl.Result{}, err
@@ -271,7 +271,7 @@ func (r *KubeadmConfigReconciler) Reconcile(req ctrl.Request) (_ ctrl.Result, re
271271
}
272272
}
273273

274-
certificates := internalcluster.NewCertificates()
274+
certificates := internalcluster.NewCertificatesForWorker(config.Spec.JoinConfiguration.CACertPath)
275275
if err := certificates.Lookup(ctx, r.Client, cluster); err != nil {
276276
log.Error(err, "unable to lookup cluster certificates")
277277
return ctrl.Result{}, err

controllers/kubeadmconfig_controller_test.go

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1267,7 +1267,10 @@ func newControlPlaneInitKubeadmConfig(machine *clusterv1.Machine, name string) *
12671267

12681268
func createSecrets(t *testing.T, cluster *clusterv1.Cluster, owner *bootstrapv1.KubeadmConfig) []runtime.Object {
12691269
out := []runtime.Object{}
1270-
certificates := internalcluster.NewCertificates()
1270+
if owner.Spec.ClusterConfiguration == nil {
1271+
owner.Spec.ClusterConfiguration = &kubeadmv1beta1.ClusterConfiguration{}
1272+
}
1273+
certificates := internalcluster.NewCertificatesForControlPlane(owner.Spec.ClusterConfiguration)
12711274
if err := certificates.Generate(); err != nil {
12721275
t.Fatal(err)
12731276
}

docs/external-etcd.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Support for external etcd
2+
3+
Cluster API Bootstrap Provider Kubeadm supports using an external etcd cluster for your workload Kubernetes clusters.
4+
5+
### ⚠️ Warnings ⚠️
6+
7+
Before getting started you should be aware of the expectations that come with using an external etcd cluster.
8+
9+
* Cluster API is unable to manage any aspect of the external etcd cluster.
10+
* Depending on how you configure your etcd nodes you may incur additional cloud costs in data transfer.
11+
* As an example, cross availability zone traffic can cost money on cloud providers. You don't have to deploy etcd
12+
across availability zones, but if you do please be aware of the costs.
13+
14+
### Getting started
15+
16+
To use this, you will need to create an etcd cluster and generate an apiserver-etcd-client key/pair.
17+
[`etcdadm`](https://github.com/kubernetes-sigs/etcdadm) is a good way to get started if you'd like to test this
18+
behavior.
19+
20+
Once you create an etcd cluster, you will want to base64 encode the `/etc/etcd/pki/apiserver-etcd-client.crt`,
21+
`/etc/etcd/pki/apiserver-etcd-client.key`, and `/etc/etcd/pki/server.crt` files and put them in two secrets. The secrets
22+
must be formatted as follows and the cert material must be base64 encoded:
23+
24+
```yaml
25+
# Kubernetes APIServer etcd client certificate
26+
kind: Secret
27+
apiVersion: v1
28+
metadata:
29+
name: $CLUSTER_NAME-apiserver-etcd-client
30+
namespace: $CLUSTER_NAMESPACE
31+
data:
32+
tls.crt: |
33+
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURCRENDQWV5Z0F3SUJBZ0lJZFlkclZUMzV0
34+
NW93RFFZSktvWklodmNOQVFFTEJRQXdEekVOTUFzR0ExVUUKQXhNRVpYUmpaREFlRncweE9UQTVN
35+
...
36+
tls.key: |
37+
LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBdlFlTzVKOE5j
38+
VCtDeGRubFR3alpuQ3YwRzByY0tETklhZzlSdFdrZ1p4MEcxVm1yClA4Zy9BRkhXVHdxSTUrNi81
39+
...
40+
```
41+
42+
```yaml
43+
# Etcd's CA crt file to validate the generated client certificates
44+
kind: Secret
45+
apiVersion: v1
46+
metadata:
47+
name: $CLUSTER_NAME-etcd
48+
namespace: $CLUSTER_NAMESPACE
49+
data:
50+
tls.crt: |
51+
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURBRENDQWVpZ0F3SUJBZ0lJRDNrVVczaDIy
52+
K013RFFZSktvWklodmNOQVFFTEJRQXdEekVOTUFzR0ExVUUKQXhNRVpYUmpaREFlRncweE9UQTVN
53+
...
54+
```
55+
56+
After that the rest is standard Kubeadm. Config your ClusterConfiguration as follows:
57+
58+
```yaml
59+
apiVersion: bootstrap.cluster.x-k8s.io/v1alpha2
60+
kind: KubeadmConfig
61+
metadata:
62+
name: CLUSTER_NAME-controlplane-0
63+
namespace: CLUSTER_NAMESPACE
64+
spec:
65+
... # initConfiguration goes here
66+
clusterConfiguration:
67+
etcd:
68+
external:
69+
endpoints:
70+
- https://10.0.0.230:2379
71+
caFile: /etc/kubernetes/pki/etcd/ca.crt
72+
certFile: /etc/kubernetes/pki/apiserver-etcd-client.crt
73+
keyFile: /etc/kubernetes/pki/apiserver-etcd-client.key
74+
... # other clusterConfiguration goes here
75+
```
76+
77+
Create your cluster as normal!

internal/cluster/certificates.go

Lines changed: 116 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"crypto/x509/pkix"
2626
"encoding/hex"
2727
"math/big"
28+
"path/filepath"
2829
"strings"
2930
"time"
3031

@@ -34,6 +35,7 @@ import (
3435
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3536
"k8s.io/client-go/util/cert"
3637
bootstrapv1 "sigs.k8s.io/cluster-api-bootstrap-provider-kubeadm/api/v1alpha2"
38+
"sigs.k8s.io/cluster-api-bootstrap-provider-kubeadm/kubeadm/v1beta1"
3739
clusterv1 "sigs.k8s.io/cluster-api/api/v1alpha2"
3840
"sigs.k8s.io/cluster-api/util/certs"
3941
"sigs.k8s.io/cluster-api/util/secret"
@@ -51,6 +53,11 @@ const (
5153

5254
// FrontProxyCA is the secret name suffix for Front Proxy CA
5355
FrontProxyCA secret.Purpose = "proxy"
56+
57+
// APIServerEtcdClient is the secret name of user-supplied secret containing the apiserver-etcd-client key/cert
58+
APIServerEtcdClient secret.Purpose = "apiserver-etcd-client"
59+
60+
defaultCertificatesDir = "/etc/kubernetes/pki"
5461
)
5562

5663
var (
@@ -67,13 +74,65 @@ var (
6774
// Certificates are the certificates necessary to bootstrap a cluster.
6875
type Certificates []*Certificate
6976

70-
// NewCertificates return an initialized but empty set of CA certificates needed to bootstrap a cluster.
71-
func NewCertificates() Certificates {
77+
// NewCertificatesForControlPlane returns a list of certificates configured for a control plane node
78+
func NewCertificatesForControlPlane(config *v1beta1.ClusterConfiguration) Certificates {
79+
if config.CertificatesDir == "" {
80+
config.CertificatesDir = defaultCertificatesDir
81+
}
82+
83+
certificates := Certificates{
84+
&Certificate{
85+
Purpose: secret.ClusterCA,
86+
CertFile: filepath.Join(config.CertificatesDir, "ca.crt"),
87+
KeyFile: filepath.Join(config.CertificatesDir, "ca.key"),
88+
},
89+
&Certificate{
90+
Purpose: ServiceAccount,
91+
CertFile: filepath.Join(config.CertificatesDir, "sa.pub"),
92+
KeyFile: filepath.Join(config.CertificatesDir, "sa.key"),
93+
},
94+
&Certificate{
95+
Purpose: FrontProxyCA,
96+
CertFile: filepath.Join(config.CertificatesDir, "front-proxy-ca.crt"),
97+
KeyFile: filepath.Join(config.CertificatesDir, "front-proxy-ca.key"),
98+
},
99+
}
100+
101+
etcdCert := &Certificate{
102+
Purpose: EtcdCA,
103+
CertFile: filepath.Join(config.CertificatesDir, "etcd", "ca.crt"),
104+
KeyFile: filepath.Join(config.CertificatesDir, "etcd", "ca.key"),
105+
}
106+
107+
// TODO make sure all the fields are actually defined and return an error if not
108+
if config.Etcd.External != nil {
109+
etcdCert = &Certificate{
110+
Purpose: EtcdCA,
111+
CertFile: config.Etcd.External.CAFile,
112+
}
113+
apiserverEtcdClientCert := &Certificate{
114+
Purpose: APIServerEtcdClient,
115+
CertFile: config.Etcd.External.CertFile,
116+
KeyFile: config.Etcd.External.KeyFile,
117+
}
118+
certificates = append(certificates, apiserverEtcdClientCert)
119+
}
120+
121+
certificates = append(certificates, etcdCert)
122+
return certificates
123+
}
124+
125+
// NewCertificatesForWorker return an initialized but empty set of CA certificates needed to bootstrap a cluster.
126+
func NewCertificatesForWorker(caCertPath string) Certificates {
127+
if caCertPath == "" {
128+
caCertPath = filepath.Join(defaultCertificatesDir, "ca.crt")
129+
}
130+
72131
return Certificates{
73-
&Certificate{Purpose: secret.ClusterCA},
74-
&Certificate{Purpose: EtcdCA},
75-
&Certificate{Purpose: ServiceAccount},
76-
&Certificate{Purpose: FrontProxyCA},
132+
&Certificate{
133+
Purpose: secret.ClusterCA,
134+
CertFile: caCertPath,
135+
},
77136
}
78137
}
79138

@@ -138,6 +197,8 @@ func (c Certificates) Generate() error {
138197
if certificate.KeyPair == nil {
139198
var generator certGenerator
140199
switch certificate.Purpose {
200+
case APIServerEtcdClient: // Do not generate the APIServerEtcdClient key pair. It is user supplied
201+
continue
141202
case ServiceAccount:
142203
generator = generateServiceAccountKeys
143204
default:
@@ -191,9 +252,10 @@ func (c Certificates) LookupOrGenerate(ctx context.Context, ctrlclient client.Cl
191252

192253
// Certificate represents a single certificate CA.
193254
type Certificate struct {
194-
Generated bool
195-
Purpose secret.Purpose
196-
KeyPair *certs.KeyPair
255+
Generated bool
256+
Purpose secret.Purpose
257+
KeyPair *certs.KeyPair
258+
CertFile, KeyFile string
197259
}
198260

199261
// Hashes hashes all the certificates stored in a CA certificate.
@@ -244,63 +306,56 @@ func (c *Certificate) AsSecret(cluster *clusterv1.Cluster, config *bootstrapv1.K
244306
return s
245307
}
246308

309+
// AsFiles converts the certificate to a slice of Files that may have 0, 1 or 2 Files.
310+
func (c *Certificate) AsFiles() []bootstrapv1.File {
311+
out := make([]bootstrapv1.File, 0)
312+
if len(c.KeyPair.Cert) > 0 {
313+
out = append(out, bootstrapv1.File{
314+
Path: c.CertFile,
315+
Owner: rootOwnerValue,
316+
Permissions: "0640",
317+
Content: string(c.KeyPair.Cert),
318+
})
319+
}
320+
if len(c.KeyPair.Key) > 0 {
321+
out = append(out, bootstrapv1.File{
322+
Path: c.KeyFile,
323+
Owner: rootOwnerValue,
324+
Permissions: "0600",
325+
Content: string(c.KeyPair.Key),
326+
})
327+
}
328+
return out
329+
}
330+
247331
// AsFiles converts a slice of certificates into bootstrap files.
248332
func (c Certificates) AsFiles() []bootstrapv1.File {
249333
clusterCA := c.GetByPurpose(secret.ClusterCA)
250334
etcdCA := c.GetByPurpose(EtcdCA)
251335
frontProxyCA := c.GetByPurpose(FrontProxyCA)
252336
serviceAccountKey := c.GetByPurpose(ServiceAccount)
253337

254-
return []bootstrapv1.File{
255-
{
256-
Path: "/etc/kubernetes/pki/ca.crt",
257-
Owner: rootOwnerValue,
258-
Permissions: "0640",
259-
Content: string(clusterCA.KeyPair.Cert),
260-
},
261-
{
262-
Path: "/etc/kubernetes/pki/ca.key",
263-
Owner: rootOwnerValue,
264-
Permissions: "0600",
265-
Content: string(clusterCA.KeyPair.Key),
266-
},
267-
{
268-
Path: "/etc/kubernetes/pki/etcd/ca.crt",
269-
Owner: rootOwnerValue,
270-
Permissions: "0640",
271-
Content: string(etcdCA.KeyPair.Cert),
272-
},
273-
{
274-
Path: "/etc/kubernetes/pki/etcd/ca.key",
275-
Owner: rootOwnerValue,
276-
Permissions: "0600",
277-
Content: string(etcdCA.KeyPair.Key),
278-
},
279-
{
280-
Path: "/etc/kubernetes/pki/front-proxy-ca.crt",
281-
Owner: rootOwnerValue,
282-
Permissions: "0640",
283-
Content: string(frontProxyCA.KeyPair.Cert),
284-
},
285-
{
286-
Path: "/etc/kubernetes/pki/front-proxy-ca.key",
287-
Owner: rootOwnerValue,
288-
Permissions: "0600",
289-
Content: string(frontProxyCA.KeyPair.Key),
290-
},
291-
{
292-
Path: "/etc/kubernetes/pki/sa.pub",
293-
Owner: rootOwnerValue,
294-
Permissions: "0640",
295-
Content: string(serviceAccountKey.KeyPair.Cert),
296-
},
297-
{
298-
Path: "/etc/kubernetes/pki/sa.key",
299-
Owner: rootOwnerValue,
300-
Permissions: "0600",
301-
Content: string(serviceAccountKey.KeyPair.Key),
302-
},
338+
certFiles := make([]bootstrapv1.File, 0)
339+
if clusterCA != nil {
340+
certFiles = append(certFiles, clusterCA.AsFiles()...)
341+
}
342+
if etcdCA != nil {
343+
certFiles = append(certFiles, etcdCA.AsFiles()...)
344+
}
345+
if frontProxyCA != nil {
346+
certFiles = append(certFiles, frontProxyCA.AsFiles()...)
347+
}
348+
if serviceAccountKey != nil {
349+
certFiles = append(certFiles, serviceAccountKey.AsFiles()...)
303350
}
351+
352+
// these will only exist if external etcd was defined and supplied by the user
353+
apiserverEtcdClientCert := c.GetByPurpose(APIServerEtcdClient)
354+
if apiserverEtcdClientCert != nil {
355+
certFiles = append(certFiles, apiserverEtcdClientCert.AsFiles()...)
356+
}
357+
358+
return certFiles
304359
}
305360

306361
func secretToKeyPair(s *corev1.Secret) (*certs.KeyPair, error) {
@@ -309,9 +364,11 @@ func secretToKeyPair(s *corev1.Secret) (*certs.KeyPair, error) {
309364
return nil, errors.Errorf("missing data for key %s", secret.TLSCrtDataName)
310365
}
311366

367+
// In some cases (external etcd) it's ok if the etcd.key does not exist.
368+
// TODO: some other function should ensure that the certificates we need exist.
312369
key, exists := s.Data[secret.TLSKeyDataName]
313370
if !exists {
314-
return nil, errors.Errorf("missing data for key %s", secret.TLSKeyDataName)
371+
key = []byte("")
315372
}
316373

317374
return &certs.KeyPair{

0 commit comments

Comments
 (0)