CDK Construct mit Projen erstellen, testen und für NPM, NuGet & PyPi veröffentlichen

Das Cloud Development Kit von AWS ist unsere Wahl um Infrastructure as Code zu verwalten. Durch den Einsatz moderner Programmiersprachen - im Vergleich zu deklarativen CloudFormation Templates - lassen sich AWS Bausteine angenehm verwalten. Sollen bestimmte Teile der Architektur wiederverwendbar sein, bietet das AWS CDK den Einsatz sogenannter Constructs.

In diesem Text zeigen wir Schritt-für-Schritt das Erstellen, Testen, und Veröffentlichen eines CDK Constructs mit projen und jsii. Das Endergebnis sind nutzbare Pakete die über NPM, NuGet, und PyPi bezogen werden können.

Grundlagen

Ein CDK Construct ist in erster Linie ein Paket, eine Bibliothek, oder einfach eine Abhängigkeit in der jeweiligen Programmiersprache des CDK. Das AWS Cloud Development Kit existiert aktuell für TypeScript, Python, Java, .NET, und Go (in Developer Preview).

Damit ein wiederverwendbares CDK Construct nicht in allen Programmiersprachen einzeln entwickelt werden muss, setzt AWS auf jsii um polyglotte Pakete aus einer Codebasis mit TypeScript zu generieren.

Projekt Aufsetzen

Als konkretes Beispiel für dieses CDK Construct dient das Erstellen eines verschlüsselten Amazon S3 Buckets mit einem eigenen AWS KMS Schlüssel. Wie so oft beginnt das Aufsetzen eines neuen Projekts mit dem Erstellen eines lokalen Ordners für alle Projektdateien, in diesem Fall encrypted-bucket.

# Neuen Ordner erstellen
$ > mkdir encrypted-bucket & cd encrypted-bucket

# Git Project initialisieren
$ > git init .

Mit Hilfe von projen kann die notwendige Projektstruktur im erstellen Verzeichnis angelegt werden. Wie viele Programme zum Bootstrappen von Projekten, bietet projen unterschiedliche Templates, wir nutzen awscdk-construct um ein neues CDK Construct anzulegen:

$ > npx projen new awscdk-construct
$ > npx projen

Neben dem verwendeten Template in TypeScript für CDK Constructs bietet projen auch weitere Project Templates offiziell an und unterstützt auch das verwenden eigener Templates.

Projektstruktur

Durch den Einsatz von projen ändern sich manche Gewohnheiten für die Verwaltung von JavaScript Paketen: Die package.json Datei wird nun nicht mehr manuell editiert, sondern wird durch projen automatisch generiert. Die gesamte Konfiguration findet in der .projenrc.js statt.

/* .projenrc.js */

const { awscdk } = require('projen');

const project = new awscdk.AwsCdkConstructLibrary({
  author: 'username',
  authorName: 'Your Name',
  authorAddress: 'mail@example.com',
  cdkVersion: '2.1.0',
  name: 'package-name',
  repositoryUrl: 'https://github.com/username/package-name.git',
  description: 'A CDK construct for reusable AWS architecture components',
  defaultReleaseBranch: 'main',
  release: true,
});

project.synth();

Die standardmäßig generierte Datei beinhaltet bereits nahezu alle notwendigen Konfigurationen für den Start. Ein paar kleinere Änderungen sind jedoch für den Start ratsam:

  • Die Felder author, authorName und authorAddress dienen zur Beschreibung der Autoren,
  • der Inhalt von name wird als Standard für das spätere NPM Paket genutzt, und
  • mit defaultReleaseBranch und release lässt sich der Release Prozess auf GitHub konfigurieren,

Nach jeder Anpassung innerhalb der .projenrc.js Datei lassen sich mit dem projen CLI die notwendigen Konfiguration generieren:

$ > npx projen

Wenn der npx projen new … Aufruf mit --projenrc-json erweitert wird, legt projen die Konfiguration im JSON Format an und --projenrc-java sorgt dafür, dass die Konfiguration mit Java geschehen kann.

AWS Komponenten

Wir erstellen also eine neue EncryptedBucket Klasse, die Construct erweitert, und darin nutzen wir die bekannten CDK Komponenten um einen S3 Bucket mit entsprechender Verschlüsselung anzulegen; die Versionierung von Objekten im Bucket kann durch einen entsprechenden Parameter aktiviert.

/* ./src/EncryptedBucket.ts */

import * as kms from 'aws-cdk-lib/aws-kms';
import * as s3 from 'aws-cdk-lib/aws-s3';
import { Construct } from 'constructs';

export interface EncryptedBucketProps {
  /**
   * Use S3 Versioning for bucket
   *
   * @default false
   */
  readonly versioned?: boolean;
}

/**
 * A CDK construct for encrypted S3 Buckets
 */
export class EncryptedBucket extends Construct {
  public bucket: s3.IBucket;
  public key: kms.IKey;

  constructor(scope: Construct, id: string, props?: EncryptedBucketProps) {
    super(scope, id);

    this.key = new kms.Key(this, 'Key');

    this.bucket = new s3.Bucket(this, 'Bucket', {
      accessControl: s3.BucketAccessControl.PRIVATE,
      bucketKeyEnabled: true,
      encryption: s3.BucketEncryption.KMS,
      encryptionKey: this.key,
      versioned: !!props?.versioned,
    });
  }
}

Testen

Das AWS Cloud Development Kit beinhaltet grundlegende Funktionen, um erstellte CDK Constructs zu testen, und durch den Einsatz von jest lässt sich der Code wie gewohnt testen. Um die Funktionalität des Constructs sicherzustellen, testet wir

  • Ob ein S3 Bucket angelegt wurde,
  • Ein KMS Schlüssel erzeugt wurde,
  • Und ob die Versionierung aktiviert/deaktiviert werden kann.

Daraus ergeben sich folgende Testszenarien:

/* ./test/EncryptedBucket.test.ts */

import { App, Stack } from 'aws-cdk-lib';
import { Template } from 'aws-cdk-lib/assertions';
import { EncryptedBucket } from '../src/EncryptedBucket';

describe('EncryptedBucket', () => {
  describe('Default', () => {
    const app = new App();
    const stack = new Stack(app, 'ExampleStack');

    new EncryptedBucket(stack, 'EncryptedBucket');

    const template = Template.fromStack(stack);

    it('creates one key', () => {
      template.resourceCountIs('AWS::KMS::Key', 1);
    });

    it('creates one bucket', () => {
      template.resourceCountIs('AWS::S3::Bucket', 1);
    });

    it('creates one bucket without versioning', () => {
      const bucket = template.findResources('AWS::S3::Bucket');
      const bucketTemplate = JSON.stringify(bucket);

      expect(bucketTemplate).not.toContain('VersioningConfiguration');
    });
  });

  describe('Versioning', () => {
    const app = new App();
    const stack = new Stack(app, 'ExampleStack');

    new EncryptedBucket(stack, 'EncryptedBucket', {
      versioned: true,
    });

    const template = Template.fromStack(stack);

    it('creates one bucket with versioning', () => {
      template.hasResourceProperties('AWS::S3::Bucket', {
        VersioningConfiguration: {
          Status: 'Enabled',
        },
      });
    });
  });
});

Die Testszenarien lassen sich mittels npx projen test ausführen:

$ > npx projen test

PASS test/EncryptedBucket.test.ts
  Default
    ✓ creates one key
    ✓ creates one bucket
    ✓ creates one bucket without versioning
  Versioning
    ✓ creates one bucket with versioning

GitHub Actions

Durch projen werden alle notwendigen Dateien generiert um mit GitHub Actions automatisiert die Tests auszuführen und den Release Prozess durchzuführen. Durch einen commit der Änderungen und dem Pushen in das Repository bei GitHub werden die Actions automatisch ausgeführt.

# Commit current project
$ > git add .
$ > git commit -m 'Initial Commit'

# Push to GitHub
$ > git remote add origin git@github.com:username/package-name.git
$ > git push origin main

Hinweis: Standardmäßig veröffentlicht projen keine Pakete über NPM, NuGet, oder PyPi; dies muss entsprechenden konfiguriert werden. Wenn release in der .projenrc.js auf true gesetzt ist, werden bei jedem Push in das Repository die Tests ausgeführt und mittels Conventional Commits und Semantic Releases neue Versionen des Pakets auf GitHub erstellt.

GitHub Actions für Projen

Deployment

Damit das erstellte CDK Construct auch verwendet werden kann, bietet es sich an, den Quellcode in einer Registry zu veröffentlichen. Dank projen muss hierzu nur die .projenrc.js Datei angepasst werden und die notwendigen Credentials zur Autorisierung in GitHub hinterlegt werden.

Wichtig: Wenn die .projenrc.js Datei editiert wurde, muss npx projen ausgeführt werden damit die Änderungen angewendet werden und eventuell notwendige Anpassungen (z.B. an der Konfiguration der GitHub Actions) durchgeführt werden.

GitHub Actions für Projen

NPM

Um das automatische Veröffentlichen auf NPM zu aktivieren, muss in der .projenrc.js der Wert für releaseToNpm gesetzt werden und in GitHub das Secret NPM_TOKEN definiert werden.

releaseToNpm: true

Nachdem das Construct auf NPM veröffentlicht wurde, lässt es sich direkt installieren:

$ > yarn add encrypted-bucket

Und danach entsprechend verwenden:

import { EncryptedBucket } from 'encrypted-bucket'

new EncryptedBucket(this, 'Bucket', {
  versioned: true
})

NuGet

Um das CDK Construct als NuGet Paket zu veröffentlichen, muss die .projenrc.js Datei um eine Konfiguration für publishToNuget erweitert werden:

publishToNuget: {
  dotNetNamespace: 'SUPERLUMINAR.AWS',
  packageId: 'CDK.EncryptedBucket',
}

Zusätzlich muss das Secret NUGET_API_KEY in GitHub angelegt werden. Sobald das Paket über NuGet veröffentlicht wurde, kann es lokal installiert werden:

$ > dotnet add package CDK.EncryptedBucket --version 0.0.8

PyPi

Auch für das Veröffentlichen als Python Paket beinhaltet projen alle notwendigen Aktionen; hierzu muss die .projenrc.js um publishToPypi erweitert werden:

publishToPypi: {
  distName: 'encrypted-bucket',
  module: 'encrypted_bucket',
}

Zusätzlich müssen die Secrets TWINE_USERNAME und TWINE_PASSWORD in GitHub gesetzt sein. Danach ist das Paket über PyPi installierbar:

$ > pip install encrypted-bucket

Abschluss

Mit diesen einfachen Schritten lassen sich die Grundlagen für polyglotte CDK Constructs legen und einfach anpassen. Dank projen und jsii werden aus TypeScript die Pakete für TypeScript, Python, und .NET erstellt und durch GitHub Actions mit Conventional Commits und Semantic Releases veröffentlicht.

Sebastian ist Senior Cloud Consultant bei superluminar GmbH, AWS Serverless Hero und AWS Certified Solutions Architect. Er schreibt hier auf Deutsch über AWS, Serverless, Software Entwicklung, Go, TypeScript und React. Englische Artikel sind auf seiner Webseite sbstjn.com oder auf Twitter unter @sbstjn zu finden.