Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add pattern for ArgoCD workloads in AWS CodeCommit #158

Merged
merged 2 commits into from
Jan 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,7 @@ Patterns:
snyk
starter
gmaestro
workloads-codecommit
```

- Bootstrap your CDK environment.
Expand Down
6 changes: 6 additions & 0 deletions bin/workloads-codecommit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import WorkloadsCodeCommitConstruct from '../lib/workloads-codecommit-construct';
import { configureApp } from '../lib/common/construct-utils';

const app = configureApp();

new WorkloadsCodeCommitConstruct(app, 'workloads-codecommit');
Binary file added docs/patterns/images/argocd-cc-workloads.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/patterns/images/argocd-cc.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
160 changes: 160 additions & 0 deletions docs/patterns/workloads-codecommit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
# EKS Cluster with ArgoCD and Workloads in private AWS CodeCommit repository

## Objective

This example shows how to provision an EKS cluster with:

- ArgoCD
- Workloads deployed by ArgoCD
- Private AWS CodeCommit repository to store the configurations of workloads
- Setup to trigger ArgoCD projects sync on git push to AWS CodeCommit repository

Pattern source: /lib/workloads-codecommit-construct/index.ts

## Architecture

![Architectural diagram](./images/argocd-cc.png)

To better understand how ArgoCD works with EKS Blueprints, read the EKS Blueprints ArgoCD [Documentation](https://aws-quickstart.github.io/cdk-eks-blueprints/addons/argo-cd/)

- After a push to AWS CodeCommit repository notification trigger calls AWS Lambda
- AWS Lambda calls ArgoCD webhook URL to trigger ArgoCD projects sync

## Prerequisites

Ensure that you have installed the following tools on your machine.

1. [aws cli](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html)
2. [kubectl](https://Kubernetes.io/docs/tasks/tools/)
3. [cdk](https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html#getting_started_install)
4. [npm](https://docs.npmjs.com/cli/v8/commands/npm-install)
5. [jq](https://jqlang.github.io/jq/)
6. `make`

## Deploy EKS Cluster with Amazon EKS Blueprints for CDK

1. Clone the repository

```sh
git clone https://github.com/aws-samples/cdk-eks-blueprints-patterns.git
cd cdk-eks-blueprints-patterns
```

2. Update npm

```sh
npm install -g npm@latest
```

3. View patterns and deploy workloads-codecommit pattern

```sh
make list
npx cdk bootstrap
make pattern workloads-codecommit deploy
```

## Verify the resources

1. Run the update-kubeconfig command. You should be able to get the command from the CDK output message. More information can be found at https://aws-quickstart.github.io/cdk-eks-blueprints/getting-started/#cluster-access

```sh
aws eks update-kubeconfig --name workloads-codecommit-blueprint --region <your region> --role-arn arn:aws:iam::xxxxxxxxx:role/workloads-codecommit-blue-workloadscodecommitbluepr-VH6YOKWPAt5H
```

2. Verify the resources created from the steps above.

```bash
$ kubectl get po -n argocd
NAME READY STATUS RESTARTS AGE
blueprints-addon-argocd-application-controller-0 1/1 Running 0 1h
blueprints-addon-argocd-applicationset-controller-7b78c7fc5dmkx 1/1 Running 0 1h
blueprints-addon-argocd-dex-server-6cf94ddc54-p68pl 1/1 Running 0 1h
blueprints-addon-argocd-notifications-controller-6f6b7d95ckhf6p 1/1 Running 0 1h
blueprints-addon-argocd-redis-b8dbc7dc6-dvbkr 1/1 Running 0 1h
blueprints-addon-argocd-repo-server-66df7f448f-kvwmw 1/1 Running 0 1h
blueprints-addon-argocd-server-584db5f545-8xp48 1/1 Running 0 1h
```

## Get ArgoCD Url and credentials

```bash
until kubectl get svc blueprints-addon-argocd-server -n argocd -o json | jq --raw-output '.status.loadBalancer.ingress[0].hostname' | grep -m 1 "elb.amazonaws.com"; do sleep 5 ; done;
export ARGOCD_SERVER=`kubectl get svc blueprints-addon-argocd-server -n argocd -o json | jq --raw-output '.status.loadBalancer.ingress[0].hostname'`
export CC_REPO_NAME=eks-blueprints-workloads-cc

echo "ArgoCD URL: https://$ARGOCD_SERVER"
echo "ArgoCD server user: admin"
echo "ArgoCD admin password: $(kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d)"
```

## Create notification trigger from AWS CodeCommit push to ArgoCD Sync

```bash
export LAMBDA_ARN=$(aws lambda get-function --function-name eks-blueprints-workloads-cc-webhook | jq -r .Configuration.FunctionArn)

cat > trigger.json <<EOF
[
{
"destinationArn": "${LAMBDA_ARN}",
"branches": [],
"name": "${CC_REPO_NAME}-trigger",
"customData": "${ARGOCD_SERVER}",
"events": [
"all"
]
}
]
EOF

aws codecommit put-repository-triggers --repository-name $CC_REPO_NAME --triggers file://trigger.json --no-cli-pager
rm trigger.json
```

## Set AWS_REGION

```bash
export AWS_REGION=$(aws ec2 describe-availability-zones --output text --query 'AvailabilityZones[0].[RegionName]')
echo $AWS_REGION
```

## Populate AWS CodeCommit with Blueprint workloads Sample repository

```bash
pushd ..
git clone https://github.com/aws-samples/eks-blueprints-workloads.git
git clone codecommit::$AWS_REGION://$CC_REPO_NAME
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's call out that AWS_REGION env var must be defined (or add a statement to initialize it from local config if not defined).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed.

cd $CC_REPO_NAME
git checkout -b main
cd ..
rsync -av eks-blueprints-workloads/ $CC_REPO_NAME --exclude .git
cd $CC_REPO_NAME
git add . && git commit -m "initial commit" && git push --set-upstream origin main
popd
```

ArgoCD will receive notification and will start sync.

![ArgoCD sync](./images/argocd-cc-workloads.png)

## Destroy

To teardown and remove the resources created in this example:

1. Delete "bootstrap-apps" project in ArgoCD UI and wait until ArgoCD delete workloads

2. Delete deployed resources

```sh
cd cdk-eks-blueprints-patterns
make pattern workloads-codecommit destroy
```

3. Delete cloned repositories (`if necessary`)

```sh
pushd ..
rm -rf eks-blueprints-workloads-cc
rm -rf eks-blueprints-workloads
popd
```
43 changes: 43 additions & 0 deletions lib/workloads-codecommit-construct/codecommit-credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId, PhysicalResourceIdReference } from 'aws-cdk-lib/custom-resources';
import { Construct } from 'constructs';

export class CodeCommitCredentials extends Construct {
readonly serviceSpecificCredentialId: string;
readonly serviceName: string;
readonly serviceUserName: string;
readonly servicePassword: string;
readonly status: string;

constructor(scope: Construct, id: string, userName: string) {
super(scope, id);

const codeCommitCredentialsResponse = new AwsCustomResource(this, "codecommit-credentials-custom-resource", {
onCreate: {
service: "IAM",
action: "createServiceSpecificCredential",
parameters: {
ServiceName: "codecommit.amazonaws.com",
UserName: userName
},
physicalResourceId: PhysicalResourceId.fromResponse("ServiceSpecificCredential.ServiceSpecificCredentialId")
},
onDelete: {
service: "IAM",
action: "deleteServiceSpecificCredential",
parameters: {
ServiceSpecificCredentialId: new PhysicalResourceIdReference(),
UserName: userName,
}
},
policy: AwsCustomResourcePolicy.fromSdkCalls({
resources: AwsCustomResourcePolicy.ANY_RESOURCE,
}),
});

this.serviceSpecificCredentialId = codeCommitCredentialsResponse.getResponseField("ServiceSpecificCredential.ServiceSpecificCredentialId");
this.serviceName = codeCommitCredentialsResponse.getResponseField("ServiceSpecificCredential.ServiceName");
this.serviceUserName = codeCommitCredentialsResponse.getResponseField("ServiceSpecificCredential.ServiceUserName");
this.servicePassword = codeCommitCredentialsResponse.getResponseField("ServiceSpecificCredential.ServicePassword");
this.status = codeCommitCredentialsResponse.getResponseField("ServiceSpecificCredential.Status");
}
}
58 changes: 58 additions & 0 deletions lib/workloads-codecommit-construct/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import * as blueprints from '@aws-quickstart/eks-blueprints';
import { Construct } from 'constructs';
import WorkloadsCodeCommitRepoStack from './workloads-codecommit-repo-stack';

/**
* Demonstrates how to use AWS CodeCommmit as a repository for ArgoCD workloads.
*/

export default class WorkloadsCodeCommitConstruct extends Construct {
constructor(scope: Construct, id: string) {
super(scope, id);

const region = process.env.CDK_DEFAULT_REGION!;

const userName = 'argocd-cc';
const repoName = 'eks-blueprints-workloads-cc';

const repoUrl = 'https://git-codecommit.' + region + '.amazonaws.com/v1/repos/' + repoName;

const stackId = `${id}-blueprint`;

const bootstrapRepo : blueprints.ApplicationRepository = {
repoUrl,
targetRevision: 'main',
credentialsSecretName: repoName + '-codecommit-secret',
credentialsType: 'TOKEN'
};

const addOns: Array<blueprints.ClusterAddOn> = [
new blueprints.NestedStackAddOn({
builder: WorkloadsCodeCommitRepoStack.builder(userName, repoName),
id: repoName + "-codecommit-repo-nested-stack"
}),
new blueprints.SecretsStoreAddOn,
new blueprints.ArgoCDAddOn({
bootstrapRepo: {
...bootstrapRepo,
path: 'envs/dev'
},
values: {
server: {
service: {
type: "LoadBalancer"
}
}
}
})
];

blueprints.EksBlueprint.builder()
.account(process.env.CDK_DEFAULT_ACCOUNT!)
.region(process.env.CDK_DEFAULT_REGION)
.addOns(...addOns)

.version('auto')
.build(scope, stackId);
}
}
52 changes: 52 additions & 0 deletions lib/workloads-codecommit-construct/lambda/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/* eslint-env node */
const https = require('https'); // eslint-disable-line

/* webhook payload
{
"ref": "refs/heads/main",
"repository": {
"html_url": "https://git-codecommit.us-west-2.amazonaws.com/v1/repos/eks-blueprints-workloads-cc",
"default_branch": "main"
}
}
*/

exports.handler = async function(event) {
const eventSourceARNarray = event.Records[0].eventSourceARN.split(':');
const repoName = eventSourceARNarray[eventSourceARNarray.length - 1];
const ref = event.Records[0].codecommit.references[0].ref;
const refArray = ref.split('/');
const branch = refArray[refArray.length - 1];
const data = JSON.stringify({
"ref": ref,
"repository": {
"html_url": "https://git-codecommit." + event.Records[0].awsRegion + ".amazonaws.com/v1/repos/" + repoName,
"default_branch": branch
}
});
console.log(data);

const options = {
hostname: event.Records[0].customData,
path: '/api/webhook',
method: 'POST',
port: 443,
headers: {
'Content-Type': 'application/json',
'X-GitHub-Event': 'push',
'Content-Length': data.length,
},
};

const promise = new Promise(function(resolve, reject) {
process.env["NODE_TLS_REJECT_UNAUTHORIZED"] = 0;
const req = https.request(options, (res) => {
resolve(res.statusCode);
}).on('error', (e) => {
reject(Error(e));
});
req.write(data);
req.end();
});
return promise;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Construct } from 'constructs';
import { NestedStack, NestedStackProps, SecretValue } from 'aws-cdk-lib';
import * as blueprints from '@aws-quickstart/eks-blueprints';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as secretsmanager from 'aws-cdk-lib/aws-secretsmanager';
import { CodeCommitCredentials } from './codecommit-credentials';

export default class WorkloadsCodeCommitRepoStack extends NestedStack {
public static builder(userName: string, repoName: string): blueprints.NestedStackBuilder {
return {
build(scope: Construct, id: string, props: NestedStackProps) {
return new WorkloadsCodeCommitRepoStack(scope, id, props, userName, repoName);
}
};
}

constructor(scope: Construct, id: string, props: NestedStackProps, userName: string, repoName: string) {
super(scope, id);

const repo = new codecommit.Repository(this, repoName + '-codecommit-repo', {
repositoryName: repoName,
});

const user = new iam.User(this, userName + '-user-name', {
userName: userName,
});
repo.grantPull(user);

const credentials = new CodeCommitCredentials(this, "codecommit-credentials", user.userName);
credentials.node.addDependency(user);

new secretsmanager.Secret(this, 'codecommit-secret', {
secretObjectValue: {
username: SecretValue.unsafePlainText(credentials.serviceUserName),
password: SecretValue.unsafePlainText(credentials.servicePassword),
url: SecretValue.unsafePlainText(repo.repositoryCloneUrlHttp)
},
secretName: repoName + '-codecommit-secret'
});

const fn = new lambda.Function(this, repoName + '-webhook', {
runtime: lambda.Runtime.NODEJS_20_X,
functionName: repoName + '-webhook',
description: 'Webhook for ArgoCD on commit to AWS CodeCommit',
handler: 'index.handler',
code: lambda.Code.fromAsset("lib/workloads-codecommit-construct/lambda"),
});

const principal = new iam.ServicePrincipal('codecommit.amazonaws.com');
fn.grantInvoke(principal);
}
}
Loading