Continuous Delivery met AWS CDK Pipelines

-

In deze blog licht ik toe hoe u uw implementatie pipeline kunt automatiseren met een aantal geavanceerde AWS-technologieën. Stap voor stap creëren we een CI/CD pipeline met behulp van AWD CDK, CodeCommit en CodePipeline die volledig is gedefinieerd in TypeScript. Aan het einde van deze blog hebben we een eenvoudig project dat de zogenaamde Pipeline als code gebruikt.

Voordat we in de details duiken, wil ik eerst de titel van deze blog ontrafelen: “Continuous delivery met AWK CDK Pipelines”. Continuous Delivery. Ik neem aan dat u een idee heeft wat Continuous Delivery betekent, maar voor de vorm definieer ik het als: de mogelijkheid om uw software op elk moment in productie te nemen. Of zoals Josh Long vaak zegt: “Productie is mijn favoriete plek op het internet. Ik ben dol op productie. U zou van productie moeten houden. U moet zo vroeg en vaak mogelijk produceren. Breng de kinderen, het hele gezin. Het weer is geweldig! Het is de gelukkigste plek op aarde!”

AWS (Amazon Web Services)

De eerst een nog steeds grootste aanbieder van openbare cloud.  CDK (Cloud Development Kit). AWK CDK is een softwareontwikkelingsraamwerk voor het definiëren van cloudinfrastructuur in code en het leveren hiervan via AWS CloudFormation. Sinds 2011 stelde CloudFormation ons in staat om de details van een infrastructuur te coderen in een configuratiebestand. Het is fijn om Infrastructure as Code te hebben, maar voor serieuze toepassingen is het veel werk om deze gigantische YAML-bestanden te onderhouden. In 2019 introduceerde AWS een abstractie bovenop Cloud Formation en noemde het de Cloud Development Kit (CDK). Deze toevoeging maakt het mogelijk om jouw Cloud Applicatie te beschrijven in een bekende programmeertaal. Momenteel ondersteunt de AWS CDK TypeScript, JavaScript, Python, Java en C#.Net. Daarnaast biedt het programma de mogelijkheid om herbruikbare gebruikerscomponenten samen te stellen en te delen, maar dat is voor vandaag niet van belang.
CDK Pipelines
Een jaar later, in juli 2020, introduceerde AWS CDK-pipelines, waardoor het eenvoudig werd om pipelines voor Continuous Delivery op te zetten met AWS CodePipeline. Met deze nieuwe CDK-constructie wordt het eenvoudig om “pipelines-as-code” te definiëren en te delen voor applicaties die automatisch uw nieuwe versies bouwen, testen en implementeren. CDK-pipelines updaten zichzelf: als u nieuwe applicatiefasen of nieuwe stacks toevoegt, configureert de pipeline zichzelf automatisch om die nieuwe fasen en/of stacks te implementeren.
Om CDK in het AWS-landschap te plaatsen:

Aan de slag

De focus van deze blog ligt op het maken van pipelines-as-code. Voor demonstratiedoeleinden zullen we een eenvoudige applicatie maken en implementeren. In wezen gaan we twee CDK-applicaties maken in één Git-repository: 1 CDK-applicatie voor de Pipeline en 1 CDK-applicatie voor de software zelf.
U kunt deze post op twee manieren lezen: 1) u leest verder, kijkt naar screenshots en gelooft dat dit is hoe het werkt of 2) u volgt de post door zelf een project te maken en de commands en codefragmenten te kopiëren. Hierdoor voelt u hoe gemakkelijk het is om een volledig geautomatiseerde pipeline voor continuous delivery te maken. Wat u ook kiest, het eindresultaat van deze blog is beschikbaar in een openbare Github-repo.

De voorbereiding

Als u nog nooit met CDK hebt gewerkt, moet u de opdrachtregelinterface installeren. Dit doet u met NPM door de volgende regel te gebruiken:
npm install -g aws-cdk
Komt u er niet uit? Raadpleeg dan de Aan de slag pagina van AWS CDK.
Wanneer u CDK voor de eerste keer in uw AWS-account gebruikt, moet CDK worden opgestart. Dit creëert een CloudFormation Stack genaamd “CDKToolkit” met alle vereiste bronnen en machtigingen voor het uitvoeren van CDK-command. Als u dit nog niet hebt gedaan, voer dan het volgende command uit:
export CDK_NEW_BOOTSTRAP=1
cdk bootstrap
Maak nu een nieuwe Git-repository in CodeCommit voor de app. Ik noemde mijn project “aws.blog.cdk-pipelines”. Nadat de repository is gemaakt, voert u deze uit in een terminal op uw computer:
git clone CODECOMMIT-REPO-URL aws.blog.cdk-pipelines
cd aws.blog.cdk-pipelines
Meer informatie over het maken van repositories in CodeCommit en hoe u deze kunt klonen, is te vinden in de AWS-documentatie. Start een nieuw project met de volgende commands:
bash
cdk init –language typescript
npm install –save-dev \
aws-cdk@1.66.0 \
@aws-cdk/aws-apigateway@1.66.0 \
@aws-cdk/aws-codebuild@1.66.0 \
@aws-cdk/aws-codecommit@1.66.0 \
@aws-cdk/aws-codedeploy@1.66.0 \
@aws-cdk/aws-codepipeline@1.66.0 \
@aws-cdk/aws-codepipeline-actions@1.66.0 \
@aws-cdk/aws-lambda@1.66.0 \
@aws-cdk/core@1.66.0 \
@aws-cdk/aws-s3@1.66.0 \

Omdat CDK-pipelines nog in de previewmodus van de ontwikkelaar staat, heb ik ervoor gekozen om specifiek versie 1.66.0 te installeren, de nieuwste versie op het moment van schrijven. Deze versie genereert de volgende mappenstructuur:

De map Bin bevat de code voor het definiëren van de applicatie. De CDK-applicatie voor de pipeline is al gegenereerd.
De map Lib bevat de daadwerkelijke Stacks. Voorlopig is het een lege Stack voor de pipeline die we later invullen.
De map Test bevat tests voor de Pipeline-applicatie. We gebruiken infra als code en een groot voordeel hiervan is dat we testbare infra als code kunnen definiëren! U kunt `npm run test` uitvoeren als u wilt. In deze post slaan we het testen van onze pipeline-as-code over, dus vervang de inhoud van `test/cdk-test.test.ts` door:

test(‘Placeholder test which never fails’, () => {
});
Doet u dit niet dan mislukt de test na de wijzigingen die we gaan aanbrengen.
Voeg tot slot de functievlag `@aws-cdk/core:newStyleStackSynthesis` toe aan het cdk.json-bestand van het project. Het bestand bevat al enkele contextwaarden; voeg deze nieuwe toe binnen het contextobject.
javascript
{

“context”: {

“@aws-cdk/core:newStyleStackSynthesis”: “true”
}
}
In de toekomstige uitgave van de AWS CDK wordt stacksynthese “nieuwe stijl” de standaard, maar voorlopig moeten we ervoor kiezen om de feature flag te gebruiken.

Creëer de pipeline

Vervang de inhoud van `lib/aws.blog.cdk-pipelines-stack.ts` met:
javascript
import {Repository} from “@aws-cdk/aws-codecommit”;
import {Artifact} from “@aws-cdk/aws-codepipeline”;
import {CdkPipeline, SimpleSynthAction} from “@aws-cdk/pipelines”;
import {CodeCommitSourceAction} from “@aws-cdk/aws-codepipeline-actions”;
import {Construct, Stack, StackProps} from “@aws-cdk/core”;

export class AwsBlogCdkPipelinesStack extends Stack {
constructor(scope: Construct, id: string, props?: StackProps) {
super(scope, id, props);

const repoName = “aws.blog.cdk-pipelines”; // Change this to the name of your repo
const repo = Repository.fromRepositoryName(this, ‘ImportedRepo’, repoName);

const sourceArtifact = new Artifact();
const cloudAssemblyArtifact = new Artifact();

const pipeline = new CdkPipeline(this, ‘Pipeline’, {
pipelineName: ‘MyAppPipeline’,
cloudAssemblyArtifact,

// Here we use CodeCommit instead of Github
sourceAction: new CodeCommitSourceAction({
actionName: ‘CodeCommit_Source’,
repository: repo,
output: sourceArtifact
}),

synthAction: SimpleSynthAction.standardNpmSynth({
sourceArtifact,
cloudAssemblyArtifact,
// Use this if you need a build step (if you’re not using ts-node
// or if you have TypeScript Lambdas that need to be compiled).
buildCommand: ‘npm run build && npm run test’,
}),
});

// Hier voegen we later de fasen voor de toepassingscode toe

}
}
Leg alle wijzigingen vast en push ze.
git add –all
git commit -m “initial commit”
git push
Draai nu ‘cdk deploy’. Dit zal een overzicht geven van alle resources die worden aangemaakt. Op de vraag “wilt u deze wijzingen implementeren?” voert u y in.
Dit kan de eerste keer even duren, dus wees geduldig. In AWS Console kunt u naar CloudFormation gaan om te zien dat een Stack met de naam ‘AwsBlogCdkPipelinesStack’ is gecreëerd en hoe deze eruit ziet. Mocht u onverklaarbare problemen tegenkomen tijdens het inzetten van de Stack, probeer dan de map ‘cdk.out’ te verwijderen en probeer het opnieuw.
Open nu CodePipeline in de AWS Console en wanneer de CloudFormation Stack is gemaakt, zou u een pipeline met 3 fasen moeten zien met de naam “AwsBlogCdkPipeline”:

1. Het bronstadium is het controleren van de Git-repository
2. De build-fase voert de Synth-actie uit, in ons geval is dat het bouwen van alle code in dit project, wat tot nu toe alleen de pipelinecode is
3. De UpdatePipeline-fase voert de SelfMutate-actie uit die de huidige pipeline verandert.
Dankzij de laatste fase was dit de laatste keer dat we het CDK-command nodig hadden. De volgende wijzigingen kunnen allemaal worden gedaan via Git-commits.

Creëer de applicatie

Laten we beginnen met het maken van de code voor de lambda. Maak `src/greeting.ts` en kopieer de volgende inhoud:
javascript
const DEPLOY_TIME = process.env.DEPLOY_TIME!
console.info(“I was deployed at: %s”, DEPLOY_TIME);

export async function handler(event: any) {
console.debug(“Received event: “, event);
return {
statusCode: 200,
body: “Hello from AWS Lambda, DEPLOY_TIME: ” + DEPLOY_TIME
};
}
Omdat we brave ontwikkelaars zijn, maken we ook een test voor onze applicatiecode. Creëer `src/greeting.test.ts` en kopieer:
javascript
import {handler} from ‘./greeting’

describe(‘Test calculationHandler’, function () {
it(‘Happy flow’, async () => {
let emptyBody = {};
let event = {body: emptyBody };

const result = await handler(event);
expect(result.statusCode).toEqual(200);
});
});
Open nu `jest.config.js` in de root van de projectmap en verwijder “/test” uit regel 2 wat resulteert in:
roots: [”],
Nu wordt onze applicatiecode getest tijdens het bouwen van onze pipeline (getriggerd door `SimpleSynthAction.standardNpmSynth ()` in ʻaws.blog.cdk-pipelines-stack.ts`). De test kan worden uitgevoerd door `npm run test` in te voeren of door op een knop in uw favoriete IDE te drukken.
Nu we een eenvoudige applicatie hebben, gaan we aan de slag met de benodigdde infra. Naast de pipeline-stack maken we een tweede stack voor onze applicatie. Creëer het bestand `lib/aws.blog-lambda-stack.ts` en kopieer de inhoud:
javascript
import {AssetCode, Function, Runtime} from “@aws-cdk/aws-lambda”
import {CfnOutput, Duration, Stack, StackProps} from ‘@aws-cdk/core’;
import {Construct} from “@aws-cdk/core/lib/construct-compat”;
import {LambdaIntegration, RestApi} from “@aws-cdk/aws-apigateway”

export class AwsBlogLambdaStack extends Stack {
public readonly urlOutput: CfnOutput;

constructor(app: Construct, id: string, props?: StackProps) {
super(app, id, props);

// Configure the lambda
const lambdaFunc = new Function(this, ‘BlogLambda’, {
code: new AssetCode(`./src`),
handler: ‘greeting.handler’,
runtime: Runtime.NODEJS_12_X,
memorySize: 256,
timeout: Duration.seconds(10),
environment: {
DEPLOY_TIME: new Date().toISOString() // Example of how we can pass variables to the deployed lambda
},
});

// Configure API in API Gateway
const api = new RestApi(this, ‘blog-greetingsApi’, {
restApiName: ‘Greeting Service’
});
// Integration with the lambda on GET method
api.root.addMethod(‘GET’, new LambdaIntegration(lambdaFunc));

// Make the URL part of the outputs of CloudFormation (see the Outputs tab of this stack in the AWS Console)
this.urlOutput = new CfnOutput(this, ‘Url’, { value: api.url, });
}
}
Dit is de definitie van de Stack van onze applicatie, die later aan de pipeline wordt toegevoegd. Hiervoor moeten we het bestand `lib / aws.blog.cdk-pipelines-stack.ts` openen.
We beginnen met het maken van een aangepaste Stage voor onze applicatie, dus kopieer de onderstaande inhoud net na alle vorige invoer (boven ‘export class AwsBlogCdkPipelinesStack`):
javascript
export class AwsBlogApplicationStage extends Stage {
public readonly urlOutput: CfnOutput;

constructor(scope: Construct, id: string, props?: StageProps) {
super(scope, id);
const lambdaStack = new AwsBlogLambdaStack(this, ‘AwsBlogLambdaStack’);
this.urlOutput = lambdaStack.urlOutput;
}
}
Later wordt duidelijk waarom hier een aangepaste Stage is gemaakt. Scroll nu naar beneden. Onder de opmerking: // Hier voegen we later de fasen voor de toepassingscode toe
javascript
let testEnv = new AwsBlogApplicationStage(this, ‘Test-env’);
const testEnvStage = pipeline.addApplicationStage(testEnv);

Wanneer u een goede IDE gebruikt, kan deze automatisch de nieuwe gebruikte klassen voor u importeren. Anders kunt u alle invoer vervangen door:
javascript
import {Repository} from “@aws-cdk/aws-codecommit”;
import {Artifact} from “@aws-cdk/aws-codepipeline”;
import {CdkPipeline, SimpleSynthAction} from “@aws-cdk/pipelines”;
import {CodeCommitSourceAction} from “@aws-cdk/aws-codepipeline-actions”;
import {CfnOutput, Construct, Stack, StackProps, Stage, StageProps} from “@aws-cdk/core”;
import {AwsBlogLambdaStack} from “./aws.blog-lambda-stack”;

Nu hebben we alles wat nodig is voor de pipeline om de stack voor de applicatiecode te implementeren. Commit en push en volg de voortgang in Pipeline van de AWS Console. Wees geduldig, want het kan even duren, vooral de eerste keer. Nadat de fase UpdatePipeline is voltooid, zou de nieuwe fase voor de Pipeline ‘Test-env’ zichtbaar moeten zijn.

Als u toegang wilt tot de geïmplementeerde API, scroll dan omlaag in de pipeline naar de laatste actie “AwsBlogLambdaStack.Deploy” en klik op “Details” om de stack in CloudFormation te openen. Ga vervolgens naar het tabblad “Outputs”. De sleutel “Url” toont de URL van uw vers geïmplementeerde API. Klik hier eens op om te zien dat het werkt.
Een andere truc om de URL van deze API op te halen is met de volgende command:
aws cloudformation describe-stacks –stack-name Test-env-AwsBlogLambdaStack \
–query “Stacks[0].Outputs[?OutputKey==’Url’].OutputValue” \
–output text
Inmiddels hebben we een eenvoudige maar fundamentele basis waarop we kunnen voortbouwen. Voordat we afronden, wil ik aantonen hoe gemakkelijk het is om deze pipeline uitgebreider te maken. Zoals je misschien hebt gemerkt heb ik de laatste fase “Test-env” genoemd. Het zou leuk zijn om ook een productieomgeving te creëren. Maar wanneer moeten we inzetten voor productie? Meestal willen we soort verificatie voordat de code wordt uitgerold naar product.

Creëer de productieomgeving

We voegen 1 geautomatiseerde verificatiestap en 1 handmatige verificatiestap toe. Als beide slagen, gaan we richting productie. Open `lib/aws.blog.cdk-pipelines-stack.js` en scroll omlaag naar regel 55, onder de regel die we “testEnvStage’ hebben gedefinieerd. Kopieer hier de volgende inhoud:
javascript
testEnvStage.addActions(
// Add automated verification step in our pipeline
new ShellScriptAction({
actionName: ‘SmokeTest’,
useOutputs: {
ENDPOINT_URL: pipeline.stackOutput(testEnv.urlOutput),
},
commands: [‘curl -Ssf $ENDPOINT_URL’],
runOrder: testEnvStage.nextSequentialRunOrder(),
}),
// Add manual verification step in our pipeline
new ManualApprovalAction({
actionName: ‘ManualApproval’,
externalEntityLink: “https://hardcoded-url.execute-api.eu-west-1.amazonaws.com/prod/”,
runOrder: testEnvStage.nextSequentialRunOrder(),
})
);

// Deploy to the Production environment
let prodEnv = new MyApplication(this, ‘Prod-env’);
const prodStage = pipeline.addApplicationStage(prodEnv);
// Extra check to be sure that the deployment to Prod was successful
prodStage.addActions(new ShellScriptAction({
actionName: ‘SmokeTest’,
useOutputs: {
ENDPOINT_URL: pipeline.stackOutput(prodEnv.urlOutput),
},
commands: [‘curl -Ssf $ENDPOINT_URL’],
}));

Nogmaals, ik hoop dat je een IDE gebruikt die de nieuwe klassen automatisch voor je kan importeren. Vervang anders alle invoer bovenop het besten door:
javascript
import {Repository} from “@aws-cdk/aws-codecommit”;
import {Artifact} from “@aws-cdk/aws-codepipeline”;
import {CdkPipeline, ShellScriptAction, SimpleSynthAction} from “@aws-cdk/pipelines”;
import {CodeCommitSourceAction, ManualApprovalAction} from “@aws-cdk/aws-codepipeline-actions”;
import {CfnOutput, Construct, Stack, StackProps, Stage, StageProps} from “@aws-cdk/core”;
import {AwsBlogLambdaStack} from “./aws.blog-lambda-stack”;

Commit en push en open de pipeline in de AWS-console om de voortgang te volgen.

Nu hebben we 2 acties toegevoegd aan de testomgeving. De eerste is ShellScriptAction die direct wordt uitgevoerd nadat de implementatie is voltooid. In dit voorbeeld gebruiken we een cURL-command als geautomatiseerde actie om te verifiëren dat onze service actief is. Deze actie mislukt wanneer het command geen HTTP 200 retourneert. Deze actie kan worden gewijzigd in het aanzetten van een shellscript uit deze repository of zelfs om een “integratietestsuite” uit te voeren.
De tweede toevoeging is handmatige verificatie. Bij deze actie blijft de pipeline staan totdat iemand deze wijziging handmatig goedkeurt of afwijst. In de schermafbeelding ziet u mogelijk een knop “Herzien”. Wanneer erop wordt geklikt, opent een pop-up geopend waarin u de wijziging kunt goedkeuren of weigeren.

Deze functie heeft zelfs een optie om de URL ter beoordeling te tonen, maar ik heb geen idee hoe we deze kunnen instellen als de URL van onze gegenereerde omgeving. In ShellScriptAction kunnen we de Stack outputs gebruiken, maar dat wordt (nog?) niet ondersteund door ManualApprovalAction. Als voorbeeld stel ik “externalEntityLink” in op een hard gecodeerde URL, maar dat wordt niet aanbevolen. Ook wanneer meerdere pushes tegelijkertijd plaatsvinden is het onduidelijk welke wijziging u beoordeelt of goedkeurt. In dergelijke scenario’s wilt u waarschijnlijk één omgeving per wijziging, maar daar kunnen we het in een andere blogpost over hebben.

Conclusie

Ik heb gedemonstreerd hoe u met slechts een paar stappen uw eigen pipeline kunt maken terwijl deze volledig is gedefinieerd in versiebeheer. Het eindresultaat van deze blogpost wordt gedeeld in deze Github-repo. Kijk zelf hoe weinig bestanden nodig waren voordat we alles in gebruik konden nemen.
Mijn ervaring is dat je zelf vrij snel een pipeline kunt maken met behulp van de nieuwe CDK-pipelineconstructie. We werken allemaal nog in de preview voor ontwikkelaars en er valt hierdoor nog het een en ander aan te passen, maar zo gaat het vaak met nieuwe services van AWS. Ze brengen nieuwe functionaliteiten naar gebruikers, luisteren naar hun feedback en verbeteren de functies vervolgens. Ik heb goede hoop dat de huidige tekortkomingen in de toekomst worden verbeterd.
Als uw projecten al in een bestaande infrastructuur draaien, zou ik deze zeker niet direct verplaatsen. Mocht u echter een nieuw AWS-project starten, maak dan gebruik van CDK voor het leveren van de bronnen en overweeg zeker om ook de CDK CodePIpeline te gebruiken. In dit voorbeeld heb ik de inferieure AWS CodeCommit gebruikt, maar u integreert tevens gemakkelijk met andere systemen zoals Github, Gitlab of Bitbucket.

De Cleanup

Om onverwachte AWS-kosten te voorkomen, moet u nadat u klaar bent uw AWS CDK-Stacks vernietigen. Open CloudFormation in de AWS-console en verwijder handmatig de stapels “Test-env-AwsBlogLambdaStack” en ” Prod-env-AwsBlogLambdaStack”.
Vernietig vervolgens de CDK-applicatie met het volgende command:
cdk destroy AwsBlogCdkPipelineStack
Hiermee verwijdert u de CloudFormation-Stack en alle bijbehorende bronnen. De gebruikte s3-buckets worden geleegd, maar niet automatisch verwijderd. Deze moeten dus nog handmatig verwijderd worden. Voor mij waren er verschillende buckets waarvan de naam begon met awsblogcdkpipelinesstack-. Verwijder tot slot de AWS CodeCommit-repository uit de AWS-console. awsblogcdkpipelinestack-pipelineartifactsbucketae-nw9nis9kkdf3

Bronnen

Deze blog is gebaseerd op het maken van een CDK-app, aangevuld met CDK Pipelines HowTo’s en een kleine toevoeging van onderzoek en eigen kennis. Raadpleeg de volgende pagina’s als u meer achtergrondinformatie wilt. Enkele gerelateerde links:
• https://cdkworkshop.com/
• https://github.com/aws-samples/aws-cdk-examples
• Other ways of deploying your code to AWS:
o https://sharing.luminis.eu/blog/the-aws-serverless-application-model-a-one-stop-shop-for-your-serverless-apps/
o https://sharing.luminis.eu/blog/amplify-your-deployment/