CINXE.COM
Providers 101: Ordering Pizza with Kubernetes and Crossplane
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" /> <title>Providers 101: Ordering Pizza with Kubernetes and Crossplane</title> <meta name="HandheldFriendly" content="True" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="/assets/css/style.css?v=a1348d70b3" /> <link rel="icon" href="/favicon.png" type="image/png" /> <link rel="canonical" href="https://blog.crossplane.io/providers-101-ordering-pizza-with-kubernetes-and-crossplane/" /> <meta name="referrer" content="no-referrer-when-downgrade" /> <link rel="amphtml" href="https://blog.crossplane.io/providers-101-ordering-pizza-with-kubernetes-and-crossplane/amp/" /> <meta property="og:site_name" content="The Crossplane Blog" /> <meta property="og:type" content="article" /> <meta property="og:title" content="Providers 101: Ordering Pizza with Kubernetes and Crossplane" /> <meta property="og:description" content="Providers 101: Learn how to write your first Crossplane provider, and bring the power of pizza to your cloud API." /> <meta property="og:url" content="https://blog.crossplane.io/providers-101-ordering-pizza-with-kubernetes-and-crossplane/" /> <meta property="og:image" content="https://blog.crossplane.io/content/images/2020/11/Crossplane_Loves_Pizza-1.png" /> <meta property="article:published_time" content="2020-11-18T18:01:25.000Z" /> <meta property="article:modified_time" content="2020-11-18T21:22:12.000Z" /> <meta property="article:tag" content="Providers" /> <meta property="article:tag" content="Universal Cloud API" /> <meta property="article:tag" content="Kubernetes" /> <meta property="article:tag" content="Crossplane" /> <meta name="twitter:card" content="summary_large_image" /> <meta name="twitter:title" content="Providers 101: Ordering Pizza with Kubernetes and Crossplane" /> <meta name="twitter:description" content="Providers 101: Learn how to write your first Crossplane provider, and bring the power of pizza to your cloud API." /> <meta name="twitter:url" content="https://blog.crossplane.io/providers-101-ordering-pizza-with-kubernetes-and-crossplane/" /> <meta name="twitter:image" content="https://blog.crossplane.io/content/images/2020/11/Crossplane_Loves_Pizza-1.png" /> <meta name="twitter:label1" content="Written by" /> <meta name="twitter:data1" content="Grant Gumina" /> <meta name="twitter:label2" content="Filed under" /> <meta name="twitter:data2" content="Providers, Universal Cloud API, Kubernetes, Crossplane" /> <meta name="twitter:site" content="@crossplane_io" /> <meta property="og:image:width" content="2000" /> <meta property="og:image:height" content="1123" /> <script type="application/ld+json"> { "@context": "https://schema.org", "@type": "Article", "publisher": { "@type": "Organization", "name": "The Crossplane Blog", "url": "https://blog.crossplane.io/", "logo": { "@type": "ImageObject", "url": "https://blog.crossplane.io/content/images/2020/05/CrossplaneLogo_Horiz-WhiteText.png" } }, "author": { "@type": "Person", "name": "Grant Gumina", "url": "https://blog.crossplane.io/author/grant/", "sameAs": [] }, "headline": "Providers 101: Ordering Pizza with Kubernetes and Crossplane", "url": "https://blog.crossplane.io/providers-101-ordering-pizza-with-kubernetes-and-crossplane/", "datePublished": "2020-11-18T18:01:25.000Z", "dateModified": "2020-11-18T21:22:12.000Z", "image": { "@type": "ImageObject", "url": "https://blog.crossplane.io/content/images/2020/11/Crossplane_Loves_Pizza-1.png", "width": 2000, "height": 1123 }, "keywords": "Providers, Universal Cloud API, Kubernetes, Crossplane", "description": "Providers 101: Learn how to write your first Crossplane provider, and bring the power of pizza to your cloud API.", "mainEntityOfPage": { "@type": "WebPage", "@id": "https://blog.crossplane.io/" } } </script> <meta name="generator" content="Ghost 4.2" /> <link rel="alternate" type="application/rss+xml" title="The Crossplane Blog" href="https://blog.crossplane.io/rss/" /> <script defer src="https://unpkg.com/@tryghost/portal@~1.1.0/umd/portal.min.js" data-ghost="https://blog.crossplane.io/"></script><style> .gh-post-upgrade-cta-content, .gh-post-upgrade-cta { display: flex; flex-direction: column; align-items: center; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; text-align: center; width: 100%; color: #ffffff; font-size: 16px; } .gh-post-upgrade-cta-content { border-radius: 8px; padding: 40px 4vw; } .gh-post-upgrade-cta h2 { color: #ffffff; font-size: 28px; letter-spacing: -0.2px; margin: 0; padding: 0; } .gh-post-upgrade-cta p { margin: 20px 0 0; padding: 0; } .gh-post-upgrade-cta small { font-size: 16px; letter-spacing: -0.2px; } .gh-post-upgrade-cta a { color: #ffffff; cursor: pointer; font-weight: 500; box-shadow: none; text-decoration: underline; } .gh-post-upgrade-cta a:hover { color: #ffffff; opacity: 0.8; box-shadow: none; text-decoration: underline; } .gh-post-upgrade-cta a.gh-btn { display: block; background: #ffffff; text-decoration: none; margin: 28px 0 0; padding: 8px 18px; border-radius: 4px; font-size: 16px; font-weight: 600; } .gh-post-upgrade-cta a.gh-btn:hover { opacity: 0.92; }</style> <style> .site-logo { max-width: 8em; } </style> <!-- Google Tag Manager --> <script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); })(window,document,'script','dataLayer','GTM-WFF2NQHG');</script> <!-- End Google Tag Manager --><style>:root {--ghost-accent-color: #F3807B;}</style> </head> <body class="post-template tag-providers tag-universal-cloud-api tag-kubernetes tag-crossplane"> <nav id="menu"> <a class="close-button">Close</a> <div class="nav-wrapper"> <p class="nav-label">Menu</p> <ul> <li class="nav-blog-home" role="presentation"><a href="https://blog.crossplane.io/">Blog Home</a></li> <li class="nav-crossplane-io" role="presentation"><a href="https://crossplane.io/">Crossplane.io</a></li> <li class="nav-subscribe-to-the-newsletter" role="presentation"><a href="https://eepurl.com/ivy4v-/">Subscribe to the Newsletter</a></li> <li class="nav-twitter"><a href="https://twitter.com/crossplane_io" title="@crossplane_io"><i class="ic ic-twitter"></i> Twitter</a></li> <li class="nav-rss"><a href="https://blog.crossplane.io/rss/"><i class="ic ic-rss"></i> Subscribe</a></li> </ul> </div> </nav> <section id="wrapper"> <a class="hidden-close"></a> <div class="progress-container"> <span class="progress-bar"></span> </div> <!-- <header id="post-header" class="has-cover" > --> <header id="post-header"> <div class="inner"> <nav id="navigation"> <span class="blog-logo"> <a href="https://blog.crossplane.io"><img src="https://blog.crossplane.io/content/images/2020/05/CrossplaneLogo_Horiz-WhiteText.png" alt="Blog Logo" /></a> </span> <span id="menu-button" class="nav-button"> <a class="menu-button"><i class="ic ic-menu"></i> Menu</a> </span> </nav> <h1 class="post-title">Providers 101: Ordering Pizza with Kubernetes and Crossplane</h1> <span class="post-meta"><a href="/author/grant/">Grant Gumina</a> | <time datetime="2020-11-18">18 Nov 2020</time></span> <!--<div class="post-cover cover" style="background-image: url('https://blog.crossplane.io/content/images/2020/11/Crossplane_Loves_Pizza-1.png');"></div>--> </div> </header> <main class="content" role="main"> <article class="post tag-providers tag-universal-cloud-api tag-kubernetes tag-crossplane"> <div class="inner"> <section class="post-content"> <p><em>This is a guest post from Grant Gumina, Principal Product Manager at Upbound who recently built provider-pizza, a Crossplane provider for the Domino’s API. In this post he shares what he learned about providers, and some common mistakes beginners might fall into when writing their first provider.</em></p><figure class="kg-card kg-image-card"><img src="https://blog.crossplane.io/content/images/2020/11/Crossplane_Loves_Pizza-2.png" class="kg-image" alt loading="lazy"></figure><p>In my day job, I work at Upbound helping customers deploy and scale Crossplane into production. Our product, Upbound Cloud is a managed service of Crossplane which lets infrastructure operators define and deploy custom cloud APIs and consoles for their teams. <br><br>Being “the product guy”, I haven’t written production code in years. However after seeing customers use Crossplane to orchestrate everything from AWS environments to GitHub ticketing systems, I wanted to use my technical background to test the limits of Crossplane's extensibility story. So I spent a weekend figuring out how to order pizza through Crossplane by building a Crossplane provider for the Domino's Pizza API.</p><p>If you're not familiar with the project, Crossplane is a cloud-native control plane which allows users to address and abstract infrastructure resources running on-premises or in the cloud(s) of your choice. It does this by installing into a Kubernetes cluster and extending the cluster's API through Providers which install into it.</p><p>Each Provider installed into the cluster running Crossplane adds cluster-scoped CRDs for various "managed resources". As a user, you can use <code>kubectl</code> to interact with these resources. For example, Crossplane lets you <code>kubectl apply -f db.yaml</code> to provision a database.</p><h2 id="the-project">The Project</h2><p>Provider-pizza is my attempt at learning more about the inner workings of Crossplane, and seeing how far I could extend the metaphor of a "universal cloud API", but it's not the focus of this post. Since Crossplane is still fairly young, there's not a ton of resources geared towards novices. I'm hoping this post can help guide aspiring provider builders in the right direction, and help you avoid some of the mistakes I made when writing my first provider.</p><p><a href="https://github.com/grantgumina/provider-pizza">View the project on GitHub</a> to learn more about how it works, see how to run it yourself, and order a delicious pizza. Keep reading if you're interested in learning more about Crossplane providers.</p><p><a href="https://github.com/grantgumina/provider-pizza/raw/master/demo.gif">https://github.com/grantgumina/provider-pizza/raw/master/demo.gif</a></p><h2 id="the-anatomy-of-a-provider">The Anatomy of a Provider</h2><p>Crossplane has an incredible extensibility story, thanks to it's provider model. Providers are basically Kubernetes-style controllers which connect anything with an API to the Kubernetes cluster where Crossplane is running, giving you a CRD representing each resource.</p><p>To build my first provider, I cloned the Crossplane team's <a href="https://github.com/crossplane/provider-template">provider-template</a> repository on GitHub, and got to work.</p><h3 id="managed-resources">Managed Resources</h3><!--kg-card-begin: markdown--><p>Inside of a Provider, you can have many different managed resources, each having it's own <code>Type</code>. In <code>provider-pizza</code>, these Managed Resources are defined in the <code>/apis</code> directory. You can see there's a Managed Resource of type <code>order</code>.</p> <!--kg-card-end: markdown--><h3 id="controllers">Controllers</h3><p>Just like a Kubernetes controller, providers run on their own reconciliation loops. The loop has several methods: </p><p><strong>Setup </strong>- Setup is called as soon as the provider starts up. It registers the controller which triggers these methods to run when events occur.</p><!--kg-card-begin: markdown--><p><strong>Connect</strong> - Generates a Crossplane <code>ExternalClient</code> which is used to connect to the managed resource. The connect method typically uses the values supplied by the user and defined in <code>ProviderConfig</code> to authenticate with the external service (typically a web API such as the Domino's pizza API). <code>ExternalClient</code> objects will interact with the external resource (in this case the Domino's API) and store connection details as a secret.</p> <!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p><strong>Create</strong> - I used this method to create the order object based out of the user input and <code>ProviderConfig</code> details. I also set a property on the Order object so I could tell whether or not an order had been requested later on in the <code>Observe</code> method. This let me return <code>ExternalObservation</code> with the appropriate information about whether or not the resource was up to date or created yet.</p> <!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p><strong>Observe</strong> - This is the mitochondria of the controller. The Observe method gets called every few seconds and updates the object's status. This is where I made calls to Domino's tracker API to get the latest information about my order. Returning an <code>ExternalObservation</code> object with <code>ResourceExists</code> set to false calls the <code>Create</code> method. This is the default behavior of <code>provider-template</code>.</p> <!--kg-card-end: markdown--><p>I <em>didn't </em>initially realize this, and the poor workers at the Wallingford Dominos fulfilled half a dozen or so orders before I caught the mistake.</p><!--kg-card-begin: markdown--><p><strong>Update</strong> - This method gets called when the <code>ResourceUpToDate</code> property on the <code>ExternalObservation</code> object is set to false. I didn't use this method for anything, but if the Domnio's API supported order modification, I suspect this is where I would handle that.</p> <!--kg-card-end: markdown--><!--kg-card-begin: markdown--><p><strong>Delete</strong> - This method gets called when the <code>ResourceExists</code> property on the <code>ExternalObservation</code> object is set to false.</p> <!--kg-card-end: markdown--><h3 id="providerconfig">ProviderConfig</h3><!--kg-card-begin: markdown--><p>Crossplane Providers can be configured with secrets for authentication or other user defined values by applying a <code>ProviderConfig</code>, a CRD type which gets installed by the user. <code>ProviderConfig</code>s have a standard format Crossplane expects to read when configuring a Provider. They can reference Kubernetes secrets, so users can store credentials needed to connect to the web services.</p> <!--kg-card-end: markdown--><p>In provider-pizza, payment information is stored as a secret and referenced by the ProviderConfig:</p><pre><code>apiVersion: v1 kind: Secret metadata: name: payment-secret namespace: crossplane-system type: Opaque data: credentials.json: ewogIGNyZWRlbnRpYWxzOiBCQVNFNjRFTkNPREVEX1BST1ZJREVSX0NSRURTLAogIFR5cGU6ICJDcmVkaXRDYXJkIiwKICBDYXJkVHlwZTogIlZpc2EiLAogIE51bWJlcjogIjExMTEyMjIyMzMzMzQ0NDQiLAogIEV4cGlyYXRpb246ICIwMTIzIiwKICBTZWN1cml0eUNvZGU6ICIxMjMiLAp9 --- apiVersion: provider-pizza.crossplane.io/v1alpha1 kind: ProviderConfig metadata: name: example spec: credentials: source: Secret secretRef: namespace: crossplane-system name: payment-secret key: payment-secret-key</code></pre><h2 id="wrapping-up">Wrapping Up</h2><p>Crossplane is designed with extensibility in mind. Typically users orchestrate cloud and on-premises infrastructure with the project, but as you can see, any service with an API can be used as well. Once installed, providers give Crossplane users a uniform interface and API to orchestrate and manipulate the managed resources they represent.</p><p>We saw how you can <code>kubectl -f apply order.yaml</code>, but you can just as easily <code>kubectl -f apply database.yaml</code> using another provider like provider-aws.</p><p>If you’re interested in learning more about how you can use Crossplane to address and abstract your own infrastructure, even if it’s not pizza related, we’d love to talk with you. Join the community Slack and follow us on Twitter.</p><p>Looking to deploy Crossplane into production, or already doing so? We’d love to give you a demo of Upbound Cloud and see how it might help with your use case. Visit us at <a href="https://upbound.io/">upbound.io</a> or email <a href="mailto:info@upbound.io">info@upbound.io</a> to learn more.</p><hr><!--kg-card-begin: markdown--><h2 id="getinvolved">Get involved!</h2> <p>We're excited to see the continual growth of the Crossplane community and would love for you to get involved. Whether you are a developer, user, or just interested in what we're up to, feel free to join us via one of the following methods:</p> <ul> <li><a href="https://crossplane.io/">Crossplane website</a></li> <li><a href="https://github.com/crossplaneio/crossplane">Github</a></li> <li><a href="https://twitter.com/crossplane_io">Twitter</a></li> <li><a href="https://slack.crossplane.io/">Slack</a></li> <li><a href="https://www.youtube.com/channel/UC19FgzMBMqBro361HbE46Fw">Youtube</a></li> <li><a href="mailto:info@crossplane.io">Email</a></li> </ul> <!--kg-card-end: markdown--> </section> <section class="post-info"> <div class="post-share"> <a class="twitter" href="https://twitter.com/share?text=Providers 101: Ordering Pizza with Kubernetes and Crossplane&url=https://blog.crossplane.io/providers-101-ordering-pizza-with-kubernetes-and-crossplane/" onclick="window.open(this.href, 'twitter-share', 'width=550,height=235');return false;"> <i class="ic ic-twitter"></i><span class="hidden">Twitter</span> </a> <a class="facebook" href="https://www.facebook.com/sharer/sharer.php?u=https://blog.crossplane.io/providers-101-ordering-pizza-with-kubernetes-and-crossplane/" onclick="window.open(this.href, 'facebook-share','width=580,height=296');return false;"> <i class="ic ic-facebook"></i><span class="hidden">Facebook</span> </a> <a class="googleplus" href="https://plus.google.com/share?url=https://blog.crossplane.io/providers-101-ordering-pizza-with-kubernetes-and-crossplane/" onclick="window.open(this.href, 'google-plus-share', 'width=490,height=530');return false;"> <i class="ic ic-googleplus"></i><span class="hidden">Google+</span> </a> <div class="clear"></div> </div> <aside class="post-tags"> <a href="/tag/providers/">Providers</a> <a href="/tag/universal-cloud-api/">Universal Cloud API</a> <a href="/tag/kubernetes/">Kubernetes</a> <a href="/tag/crossplane/">Crossplane</a> </aside> <div class="clear"></div> <aside class="post-author"> <figure class="post-author-avatar avatar"> </figure> <div class="post-author-bio"> <h4 class="post-author-name"><a href="/author/grant/">Grant Gumina</a></h4> </div> <div class="clear"></div> </aside> </section> <!-- <section class="post-comments"> <a id="show-disqus" class="post-comments-activate">Show Comments</a> <div id="disqus_thread"></div> </section> --> <!-- Begin Mailchimp Signup Form --> <link href="//cdn-images.mailchimp.com/embedcode/classic-10_7.css" rel="stylesheet" type="text/css"> <style type="text/css"> #mc_embed_signup{background:#fff; clear:left; font:14px Helvetica,Arial,sans-serif; width: 600px; align-content: center; } /* Add your own Mailchimp form style overrides in your site stylesheet or in this style block. We recommend moving this block and the preceding CSS link to the HEAD of your HTML file. */ </style> <div id="mc_embed_signup"> <form action="https://upbound.us17.list-manage.com/subscribe/post?u=b9f6c1840c97ee09ae739fdb0&id=4f555f7090" method="post" id="mc-embedded-subscribe-form" name="mc-embedded-subscribe-form" class="validate" target="_blank" novalidate> <div id="mc_embed_signup_scroll"> <h2>Keep up with Upbound</h2> <div class="indicates-required"><span class="asterisk">*</span> indicates required</div> <div class="mc-field-group"> <label for="mce-LNAME">Name <span class="asterisk">*</span> </label> <input type="text" value="" name="LNAME" class="required" id="mce-LNAME"> </div> <div class="mc-field-group"> <label for="mce-EMAIL">Email Address <span class="asterisk">*</span> </label> <input type="email" value="" name="EMAIL" class="required email" id="mce-EMAIL"> </div> <div id="mce-responses" class="clear"> <div class="response" id="mce-error-response" style="display:none"></div> <div class="response" id="mce-success-response" style="display:none"></div> </div> <!-- real people should not fill this in and expect good things - do not remove this or risk form bot signups--> <div style="position: absolute; left: -5000px;" aria-hidden="true"><input type="text" name="b_b9f6c1840c97ee09ae739fdb0_4f555f7090" tabindex="-1" value=""></div> <div class="clear"><input type="submit" value="Subscribe" name="subscribe" id="mc-embedded-subscribe" class="button"></div> </div> </form> </div> <script type='text/javascript' src='//s3.amazonaws.com/downloads.mailchimp.com/js/mc-validate.js'></script><script type='text/javascript'>(function($) {window.fnames = new Array(); window.ftypes = new Array();fnames[2]='LNAME';ftypes[2]='text';fnames[0]='EMAIL';ftypes[0]='email';}(jQuery));var $mcj = jQuery.noConflict(true);</script> <!--End mc_embed_signup--> <aside class="post-nav"> <a class="post-nav-next" href="/crossplane-turns-2-years-old-just-ahead-of-v1-release/"> <section class="post-nav-teaser"> <i class="ic ic-arrow-left"></i> <h2 class="post-nav-title">Crossplane turns 2 years old just ahead of v1.0 release!</h2> <p class="post-nav-excerpt">Come join us to celebrate at the second Crossplane Community Day on…</p> </section> </a> <a class="post-nav-prev" href="/accelerating-crossplane-provider-coverage-with-ack-and-azure-code-generation-towards-100-percent-coverage-of-all-cloud-services/"> <section class="post-nav-teaser"> <i class="ic ic-arrow-right"></i> <h2 class="post-nav-title">Accelerating Crossplane provider coverage with ACK and Azure code generation!</h2> <p class="post-nav-excerpt">The Crossplane community is making excellent progress towards code generating native Crossplane…</p> </section> </a> <div class="clear"></div> </aside> </div> </article> </main> <div id="body-class" style="display: none;" class="post-template tag-providers tag-universal-cloud-api tag-kubernetes tag-crossplane"></div> <footer id="footer"> <div class="inner"> <section class="credits"> <span class="credits-theme">Theme <a href="https://github.com/zutrinken/attila">Attila</a> by <a href="http://zutrinken.com" rel="nofollow">zutrinken</a></span> <span class="credits-software">Published with <a href="http://ghost.org">Ghost</a></span> </section> </div> </footer> </section> <script type="text/javascript" src="/assets/js/script.js?v=a1348d70b3"></script> <!-- Google Tag Manager (noscript) --> <noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-WFF2NQHG" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript> <!-- End Google Tag Manager (noscript) --> </body> </html>