Publishing Resources using Crossplane
This guide describes the process of leveraging Crossplane as a service provider to make Crossplane
claims available as PublishedResources
for use in KDP. This involves installing Crossplane -
including all required Crossplane providers and
configuration packages - and
publishing (a subset of) the Crossplane claims.
Overview
The api-syncagent is responsible for synchronizing objects from
KDP to the local service cluster where the service provider is in charge of processing these
synchronized objects to provide the actual functionality of a service. One possibility is to leverage
Crossplane to create new abstractions and custom APIs, which can be published to KDP and consumed by
platform users.
[!NOTE]
While this guide is not intended to be a comprehensive Crossplane guide, it is useful to be aware
of the most common terms:
- Providers are pluggable building blocks to provision and manage resources via a third-party API (e.g. AWS provider)
- Managed resources (MRs) are representations of actual, provider-specific resources (e.g. EC2 instance)
- Composite resource definitions (XRDs) are Crossplane-specific definitions of API resources (similar to CRDs)
- Composite resources (XRs) and Claims are Crossplane-specific custom resources created from XRD objects (similar to CRs)
- Compositions are Crossplane-specific templates for transforming a XR object into one or more MR object(s)
This guide will show you how to install Crossplane and all required providers on a service cluster
and provide a stripped-down Certificate
resource in KDP. While we ultimately use cert-manager to
provide the actual TLS certificates, we will expose only a very limited number of fields of the
cert-manager Certificate
to the platform users - in fact a single field to set the desired common
name.
[!NOTE]
The Upbound marketplace provides a list of available
configuration packages (reusable packages of compositions and XRDs), but at the time of writing
no suitable configuration package that relies only on the Kubernetes / Helm provider and works
out of the box was available.
Install Crossplane
First we need to install Crossplane via the official Helm chart. By
default, Crossplane does not require any special configuration so we will just use the default
values provided by the Helm chart.
helm upgrade crossplane crossplane \
--install \
--create-namespace \
--namespace=crossplane-system \
--repo=https://charts.crossplane.io/stable \
--version=1.15.0 \
--wait
Once the installation is done, verify the status with the following command:
$ kubectl get pods --namespace=crossplane-system
NAME READY STATUS RESTARTS AGE
crossplane-6494656b8b-bflcf 1/1 Running 0 45s
crossplane-rbac-manager-8458557cdd-sls58 1/1 Running 0 45s
Install Crossplane providers
With Crossplane up and running, we can continue and install the necessary Crossplane packages
(providers), composite resource definitions, and compositions.
In order to manage arbitrary Kubernetes objects with Crossplane (and leverage cert-manager to
issue TLS certificates), we are going to install the provider-kubernetes
on the service cluster.
Additionally (and for the sake of simplicity), we create a DeploymentRuntimeConfig
to assign the
provider a specific service account, which can be used to assign the required permissions.
kubectl apply --filename=- <<EOF
---
apiVersion: pkg.crossplane.io/v1beta1
kind: DeploymentRuntimeConfig
metadata:
name: crossplane-provider-kubernetes
spec:
serviceAccountTemplate:
metadata:
name: crossplane-provider-kubernetes
---
apiVersion: pkg.crossplane.io/v1
kind: Provider
metadata:
name: crossplane-provider-kubernetes
labels:
app.kubernetes.io/component: provider
spec:
package: xpkg.upbound.io/crossplane-contrib/provider-kubernetes:v0.11.1
runtimeConfigRef:
name: crossplane-provider-kubernetes
EOF
Once the provider is installed, verify the provider status with the following command:
$ kubectl get providers crossplane-provider-kubernetes
NAME INSTALLED HEALTHY PACKAGE AGE
crossplane-provider-kubernetes True True xpkg.upbound.io/crossplane-contrib/provider-kubernetes:v0.11.1 104s
With the provider-kubernetes
in place, we assign the provider-specific service account
cluster-admin permissions (you know, for the sake of simplicity) and create a ProviderConfig
to instruct the provider to use the provided service account token for authentication.
kubectl apply --filename=- <<EOF
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: crossplane:provider:crossplane-provider-kubernetes:cluster-admin
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: crossplane-provider-kubernetes
namespace: crossplane-system
---
apiVersion: kubernetes.crossplane.io/v1alpha1
kind: ProviderConfig
metadata:
name: in-cluster
spec:
credentials:
source: InjectedIdentity
EOF
Install cert-manager
Now that Crossplane and all required providers are installed and properly configured, we can
install cert-manager and apply our own CompositeResourceDefinition
to the service cluster.
We install cert-manager via the official Helm chart including all CRDs.
helm upgrade cert-manager cert-manager \
--install --create-namespace \
--namespace=cert-manager \
--repo=https://charts.jetstack.io \
--version=v1.14.2 \
--set=installCRDs=true \
--wait
Define Crossplane claims
Once cert-manager is installed, we can finally define our own stripped-down Certificate
resource
and provide a default Crossplane composition, which creates a cert-manager Certificate
for each
Crossplane specific Certificate
object.
Create and apply the following three manifests to your service cluster (you can safely ignore the
misleading warnings from Crossplane regarding the validation of the composition). This will
- bootstrap a cert-manager
ClusterIssuer
named “default-ca”, - create a Crossplane
CompositeResourceDefinition
that defines our Certificate
resource (which exposes only the requested common name), - create a Crossplane
Composition
that uses cert-manager and the created “default-ca” to issue the requested certificate
kubectl apply --filename=cluster-issuer.yaml
kubectl apply --filename=definition.yaml
kubectl apply --filename=composition.yaml
---
apiVersion: cert-manager.io/v1
kind: Issuer
metadata:
name: default-bootstrap-ca
namespace: cert-manager
spec:
selfSigned: {}
---
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: default-ca
namespace: cert-manager
spec:
isCA: true
commonName: default-ca
secretName: default-ca
privateKey:
algorithm: ECDSA
size: 256
issuerRef:
group: cert-manager.io
kind: Issuer
name: default-bootstrap-ca
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: default-ca
spec:
ca:
secretName: default-ca
apiVersion: apiextensions.crossplane.io/v1
kind: CompositeResourceDefinition
metadata:
name: xcertificates.pki.xaas.k8c.io
spec:
group: pki.xaas.k8c.io
names:
kind: XCertificate
plural: xcertificates
claimNames:
kind: Certificate
plural: certificates
connectionSecretKeys:
- ca.crt
- tls.crt
- tls.key
versions:
- name: v1alpha1
served: true
referenceable: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- parameters
properties:
parameters:
type: object
required:
- commonName
properties:
commonName:
description: "Requested common name X509 certificate subject attribute. More info: https://datatracker.ietf.org/doc/html/rfc5280#section-4.1.2.6 NOTE: TLS clients will ignore this value when any subject alternative name is set (see https://tools.ietf.org/html/rfc6125#section-6.4.4). \n Should have a length of 64 characters or fewer to avoid generating invalid CSRs. Cannot be set if the `literalSubject` field is set."
type: string
minLength: 1
apiVersion: apiextensions.crossplane.io/v1
kind: Composition
metadata:
name: v1alpha1.xcertificates.cert-manager.pki.xaas.k8c.io
labels:
xaas.k8c.io/provider-name: cert-manager
spec:
compositeTypeRef:
apiVersion: pki.xaas.k8c.io/v1alpha1
kind: XCertificate
resources:
- name: certificate
base:
apiVersion: kubernetes.crossplane.io/v1alpha2
kind: Object
spec:
forProvider:
manifest:
apiVersion: cert-manager.io/v1
kind: Certificate
spec:
issuerRef:
group: cert-manager.io
kind: ClusterIssuer
name: default-ca
readiness:
policy: DeriveFromObject
providerConfigRef:
name: in-cluster
connectionDetails:
- apiVersion: v1
kind: Secret
namespace: __PATCHED__
name: __PATCHED__
fieldPath: data['ca.crt']
toConnectionSecretKey: ca.crt
- apiVersion: v1
kind: Secret
namespace: __PATCHED__
name: __PATCHED__
fieldPath: data['tls.crt']
toConnectionSecretKey: tls.crt
- apiVersion: v1
kind: Secret
namespace: __PATCHED__
name: __PATCHED__
fieldPath: data['tls.key']
toConnectionSecretKey: tls.key
writeConnectionSecretToRef:
namespace: crossplane-system
patches:
# spec.forProvider.manifest.metadata
- type: FromCompositeFieldPath
fromFieldPath: spec.claimRef.namespace
toFieldPath: spec.forProvider.manifest.metadata.namespace
policy:
fromFieldPath: Required
# spec.forProvider.manifest.spec
- type: FromCompositeFieldPath
fromFieldPath: spec.parameters.commonName
toFieldPath: spec.forProvider.manifest.spec.commonName
policy:
fromFieldPath: Required
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.forProvider.manifest.spec.secretName
policy:
fromFieldPath: Required
# spec.connectionDetails
- type: FromCompositeFieldPath
fromFieldPath: spec.claimRef.namespace
toFieldPath: spec.connectionDetails[*].namespace
policy:
fromFieldPath: Required
- type: FromCompositeFieldPath
fromFieldPath: metadata.name
toFieldPath: spec.connectionDetails[*].name
policy:
fromFieldPath: Required
# spec.writeConnectionSecretToRef
- type: FromCompositeFieldPath
fromFieldPath: metadata.uid
toFieldPath: spec.writeConnectionSecretToRef.name
policy:
fromFieldPath: Required
transforms:
- type: string
string:
type: Format
fmt: "%s-certificate"
connectionDetails:
- name: ca.crt
type: FromConnectionSecretKey
fromConnectionSecretKey: ca.crt
- name: tls.crt
type: FromConnectionSecretKey
fromConnectionSecretKey: tls.crt
- name: tls.key
type: FromConnectionSecretKey
fromConnectionSecretKey: tls.key
writeConnectionSecretsToNamespace: crossplane-system
Afterwards verify the status of the composite resource definition and the composition with the
following command:
$ kubectl get compositeresourcedefinitions,compositions
NAME ESTABLISHED OFFERED AGE
xcertificates.pki.xaas.k8c.io True True 10s
NAME XR-KIND XR-APIVERSION AGE
v1alpha1.xcertificates.cert-manager.pki.xaas.k8c.io XCertificate pki.xaas.k8c.io/v1alpha1 17s
Additionally before we continue and publish our Certificate
resource to KDP, you can verify that
everything is working as expected on the service cluster by applying the following example
certificate manifest:
kubectl apply --filename=- <<EOF
apiVersion: pki.xaas.k8c.io/v1alpha1
kind: Certificate
metadata:
name: www-example-com
spec:
parameters:
commonName: www.example.com
writeConnectionSecretToRef:
name: www-example-com
EOF
Crossplane will (stay with me) pick up the Certificate
object (claim), create a corresponding
XCertificate
object (composite resource), apply our created composition to the composite resource,
which in turn will create a Object
object (managed resource), which is picked up by the
provider-kubernetes
, which will create finally a cert-manager Certificate
object (halfway through).
graph LR
subgraph "Crossplane"
A("Certificate <br/> (Claim)") --> B("XCertificate <br/> (XR)")
C("v1alpha1.xcertificate <br/> (Composition)") --> B --> C
end
subgraph "provider-kubernetes"
D("Object <br/> (MR)")
end
subgraph "cert-manager"
E(Certificate)
end
C --> D --> E
Now provider-kubernetes
will wait for the secret containing the actual signed TLS certificate
issued by cert-manager, copy it into an intermediate secret (connection secret) in the
crossplane-system
namespace for further processing, that will be picked up by Crossplane, which
will copy the information into the secret (combined secret) defined in the Certificate
object by
spec.writeConnectionSecretToRef.name
(phew you made it).
graph RL
subgraph "Crossplane"
A("Secret <br/> (Combined secret)")
end
subgraph "provider-kubernetes"
B("Secret <br/> (Connection secret)")
end
subgraph "cert-manager"
C("Secret <br/> (TLS certificate)")
end
A --> B --> C
If everything worked out, you should get all relevant objects with the following command:
$ kubectl get claim,composite,managed,certificate
NAME SYNCED READY CONNECTION-SECRET AGE
certificate.pki.xaas.k8c.io/www-example-com True True www-example-com 21m
NAME SYNCED READY COMPOSITION AGE
xcertificate.pki.xaas.k8c.io/www-example-com-z59kn True True v1alpha1.xcertificates.cert-manager.pki.xaas.k8c.io 21m
NAME KIND PROVIDERCONFIG SYNCED READY AGE
object.kubernetes.crossplane.io/www-example-com-z59kn-8wcmd Certificate in-cluster True True 21m
NAME READY SECRET AGE
certificate.cert-manager.io/www-example-com-z59kn-8wcmd True www-example-com-z59kn 21m
Publish Crossplane claims
Now onto the final step: making our custom Certificate
available in KDP. This can be achieved by
simply applying the following manifest to the service cluster.
kubectl apply --filename=- <<'EOF'
apiVersion: services.kdp.k8c.io/v1alpha1
kind: PublishedResource
metadata:
name: v1alpha1.certificate.pki.xaas.k8c.io
spec:
naming:
name: $remoteName
namespace: certs-$remoteClusterName-$remoteNamespaceHash
related:
- kind: Secret
origin: service
reference:
name:
path: spec.writeConnectionSecretToRef.name
resource:
apiGroup: pki.xaas.k8c.io
kind: Certificate
version: v1alpha1
EOF
And done! The api-syncagent will pick up the PublishedResource
object, set up the corresponding kcp
APIExport
and APIResourceSchema
and begin syncing objects from KDP to your service cluster.
For more information, see the guide on publishing resources.