Skip to content

Commit a4d55d0

Browse files
Start Basic Pulumi Typescript Implementation (#39)
* Start Basic Pulumi Typescript Implementation Signed-off-by: Blake Romano <[email protected]> * Update README.md (#40) Signed-off-by: Blake Romano <[email protected]> * update crossplane pattern (#44) * update crossplane --------- Signed-off-by: Carlos Santana <[email protected]> Signed-off-by: Blake Romano <[email protected]> * update gitops-bridge refernce from git to terraform registry (#46) Signed-off-by: Carlos Santana <[email protected]> Signed-off-by: Blake Romano <[email protected]> * add argo workflow Signed-off-by: Carlos Santana <[email protected]> Signed-off-by: Blake Romano <[email protected]> * Argo workflows ingress (#47) * initial work argo-workflows Signed-off-by: Carlos Santana <[email protected]> Signed-off-by: Blake Romano <[email protected]> * Update README.md (#48) Signed-off-by: Blake Romano <[email protected]> * update argo workflows (#49) Signed-off-by: Carlos Santana <[email protected]> Signed-off-by: Blake Romano <[email protected]> * Karpenter pattern (#50) * update argo workflows Signed-off-by: Carlos Santana <[email protected]> * add karpenter pattern Signed-off-by: Carlos Santana <[email protected]> * make eks 1.29 Signed-off-by: Carlos Santana <[email protected]> --------- Signed-off-by: Carlos Santana <[email protected]> Signed-off-by: Blake Romano <[email protected]> * Update README.md (#51) Signed-off-by: Blake Romano <[email protected]> * Update Pulumi Implementation Details to be more specific, add bootstrap command. Signed-off-by: Blake Romano <[email protected]> --------- Signed-off-by: Blake Romano <[email protected]> Signed-off-by: Carlos Santana <[email protected]> Co-authored-by: Carlos Santana <[email protected]>
1 parent 87d1931 commit a4d55d0

File tree

12 files changed

+402
-0
lines changed

12 files changed

+402
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/bin/
2+
/node_modules/
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
config:
2+
aws:region: us-east-1
3+
cidrBlock: 10.0.0.0/16
4+
awsAccountId: "54321"
5+
clusterType: "spoke"
6+
hubStackName: "hub"
7+
githubOrg: "gitops-bridge"
8+
githubRepo: "repoName"
9+
secretPath: "foo/bar/spoke.yaml"
10+
implementationType: "github"
11+
clusterComponents: {}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
config:
2+
aws:region: us-east-1
3+
awsAccountId: "1234"
4+
cidrBlock: 10.0.0.0/16
5+
githubOrg: "gitops-bridge"
6+
githubRepo: "repoName"
7+
secretPath: "hub.yaml"
8+
clusterType: "hub"
9+
implementationType: "github"
10+
clusterComponents: {}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name: gitops-bridge-ts
2+
runtime: nodejs
3+
description: GitOps Bridge Implementation with Pulumi Typescript
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# Pulumi Typescript GitOps Bridge
2+
3+
### How to Start Your Hub Cluster:
4+
5+
1. Create applicable stack files you need - The `Pulumi.dev.yaml` and `Pulumi.hub.yaml` each correspond to one stack for your hub cluster and one stack for a development environment spoke cluster
6+
2. Create a GitOps Repo - Add a README.md and stub out the files you will be adding the ArgoCD Cluster secrets to.
7+
3. Update configuration values as you need - You will want to update Stack Files with configuration for Github Repo/Org, as well as AWS Account ID, CIDRs, etc;
8+
4. Add any extra resources you may need in your given environment
9+
5. Add an Environment Variable for `GITHUB_TOKEN` in your deployment env (local, Github Actions, AWS Code Pipeline, etc;)
10+
6. `pulumi up --stack hub`
11+
7. Wait for the Resources to create like VPC, EKS Cluster, and IAM permissions
12+
8. Run `./bootstrap.sh`
13+
14+
15+
### How to Add Spoke Clusters:
16+
17+
1. Add any extra resources you may need in your given environment
18+
2. Add an Environment Variable for `GITHUB_TOKEN` in your deployment env (local, Github Actions, AWS Code Pipeline, etc;)
19+
3. Run Pulumi Up for the Spoke Cluster's Stack `pulumi up --stack dev`
20+
4. Wait for the Resources to create like VPC, EKS Cluster, and IAM permissions
21+
5. Apply the Secret resource that was added to the GitOps Repository
22+
23+
### Productionizing your Implementation
24+
25+
* Add Authentication for ArgoCD to be able to grab from your Organization's private repository
26+
* Add ApplicationSets to your configuration by looking at the GitOps Bridge Control Plane Template for resources you need
27+
* Create an ArgoCD Application that manages deployment of your Cluster Secret
28+
* Move your EKS Cluster to be a private access endpoint
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#!/bin/bash
2+
3+
export AWS_PROFILE=default
4+
5+
# Load config from YAML
6+
get_value() {
7+
yq eval ".config.$1" Pulumi.hub.yaml
8+
}
9+
10+
# Load config from YAML
11+
githubOrg=$(get_value "githubOrg")
12+
githubRepo=$(get_value "githubRepo")
13+
secretPath=$(get_value "secretPath")
14+
15+
# Clone the GitHub repo
16+
git clone "https://github.com/$githubOrg/$githubRepo.git"
17+
18+
# Add EKS cluster to kubeconfig
19+
aws eks --region us-east-1 update-kubeconfig --name hub-cluster --alias hub-cluster
20+
21+
# Install ArgoCD
22+
kubectl create namespace argocd --context hub-cluster
23+
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml --context hub-cluster
24+
25+
kubectl apply -f "$githubRepo/$secretPath"
26+
27+
# Echo command to port forward ArgoCD and get admin password
28+
echo "To port forward ArgoCD run: kubectl -n argocd port-forward svc/argocd-server 8080:443 &"
29+
echo "Password can be retrieved by running: kubectl -n argocd get secret argocd-initial-admin-secret -o jsonpath="{.data.password}" | base64 -d"
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import * as pulumi from "@pulumi/pulumi";
2+
import * as github from "@pulumi/github";
3+
import * as yaml from 'js-yaml';
4+
import { getValue } from "./utils"
5+
import { env } from "process";
6+
7+
export class GitOpsClusterConfig {
8+
private config: pulumi.Config;
9+
private outputs: {[key: string]: pulumi.Output<any>};
10+
11+
constructor(outputs: {[key: string]: pulumi.Output<any>}, config: pulumi.Config, clusterAuthority: pulumi.Output<string>) {
12+
this.config = config
13+
this.outputs = outputs
14+
const annotations = this.generateAnnotations(pulumi.getStack())
15+
const serverConfig = this.generateConfig(clusterAuthority)
16+
getValue(pulumi.all([annotations, serverConfig]).apply(([annotations, serverConfig]) => {
17+
return {
18+
apiVersion: "v1",
19+
kind: "Secret",
20+
metadata: {
21+
labels: this.generateLabels(),
22+
annotations: annotations,
23+
name: `${pulumi.getStack()}-cluster-secret`,
24+
namespace: "argocd",
25+
},
26+
type: "Opaque",
27+
stringData: {
28+
name: pulumi.getStack(),
29+
server: config.require("clusterType") === "hub" ? "https://kubernetes.default.svc" : `https://${annotations.k8s_service_host}`
30+
},
31+
data: {
32+
config: Buffer.from(serverConfig).toString("base64"),
33+
},
34+
}
35+
})).then(fileContents => {
36+
const provider = new github.Provider("github", {
37+
token: env.GITHUB_TOKEN,
38+
owner: config.require("githubOrg"),
39+
})
40+
new github.RepositoryFile("argo-cluster-secret.yaml", {
41+
repository: config.require("githubRepo"),
42+
file: config.require("secretPath"),
43+
content: yaml.dump(fileContents),
44+
branch: "main",
45+
commitMessage: `Update Argo Config Secret for ${pulumi.getStack()}`,
46+
overwriteOnCreate: true,
47+
}, {provider: provider});
48+
})
49+
.catch(err => console.log(err))
50+
}
51+
52+
private generateConfig(clusterAuthority: pulumi.Output<string>) {
53+
if (this.config.require("clusterType") !== "hub") {
54+
return pulumi.all([this.outputs.argoRoleArn, clusterAuthority]).apply(([argoRoleArn, clusterAuthority]) => `{
55+
"awsAuthConfig": {
56+
"clusterName": "${this.config.require("name")}-cluster",
57+
"roleARN": "${argoRoleArn}"
58+
},
59+
"tlsClientConfig": {
60+
"insecure": false,
61+
"caData": "${clusterAuthority}"
62+
}
63+
}
64+
`)
65+
}
66+
return `{
67+
"tlsClientConfig": {
68+
"insecure": false
69+
}
70+
}
71+
`
72+
}
73+
74+
private generateAnnotations(name: string) {
75+
// Add More Outputs as needed to output to cluster secret
76+
const outputs = pulumi.all([
77+
this.outputs.clusterName,
78+
this.outputs.clusterApiEndpoint,
79+
])
80+
const annotations = outputs.apply(([
81+
clusterName,
82+
clusterApiEndpoint,
83+
]) => {
84+
return {
85+
"aws_cluster_name": clusterName,
86+
"k8s_service_host": clusterApiEndpoint.split("://")[1],
87+
}
88+
})
89+
return annotations
90+
}
91+
92+
private generateLabels() {
93+
return {
94+
"argocd.argoproj.io/secret-type": "cluster",
95+
...this.config.requireObject<Object>("clusterComponents"),
96+
}
97+
}
98+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as pulumi from "@pulumi/pulumi";
2+
import * as aws from "@pulumi/aws";
3+
4+
export function createArgoRole(
5+
awsAccountId: string,
6+
oidcProviderUrl: pulumi.Output<any>,
7+
config: pulumi.Config,
8+
) {
9+
if (config.require("clusterType") === "spoke") {
10+
const hubStack = new pulumi.StackReference("hub-argorole-ref", {
11+
name: config.require("hubStackName")
12+
})
13+
const outputs = hubStack.getOutput("outputs") as pulumi.Output<{[key: string]: string}>
14+
const policy = outputs.apply((outputs) => JSON.stringify({
15+
Version: "2012-10-17",
16+
Statement: [
17+
{
18+
Effect: "Allow",
19+
Principal: {
20+
AWS: outputs.argoRoleArn
21+
},
22+
Action: "sts:AssumeRole"
23+
}
24+
]
25+
}))
26+
return new aws.iam.Role("argo-role", {
27+
assumeRolePolicy: policy
28+
})
29+
}
30+
return new aws.iam.Role("argo-role", {
31+
inlinePolicies: [
32+
{
33+
name: "Argo",
34+
policy: JSON.stringify({
35+
Version: "2012-10-17",
36+
Statement: [
37+
{
38+
Sid: "ArgoSecrets",
39+
Action: [
40+
"secretsmanager:List*",
41+
"secretsmanager:Read*"
42+
],
43+
Resource: "*",
44+
Effect: "Allow",
45+
},
46+
{
47+
Sid: "AssumeRoles",
48+
Action: [
49+
"sts:AssumeRole"
50+
],
51+
Resource: "*",
52+
Effect: "Allow"
53+
},
54+
],
55+
})
56+
}
57+
],
58+
assumeRolePolicy: oidcProviderUrl.apply(v => JSON.stringify({
59+
Version: "2012-10-17",
60+
Statement: [
61+
{
62+
Effect: "Allow",
63+
Principal: {
64+
Federated: `arn:aws:iam::${awsAccountId}:oidc-provider/${v}`
65+
},
66+
Action: "sts:AssumeRoleWithWebIdentity",
67+
Condition: {
68+
StringLike: {
69+
[`${v}:sub`]: ["system:serviceaccount:argocd:argocd-application-controller", "system:serviceaccount:argocd:argocd-server"],
70+
[`${v}:aud`]: "sts.amazonaws.com"
71+
}
72+
}
73+
}
74+
]
75+
}))
76+
})
77+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import * as pulumi from "@pulumi/pulumi";
2+
import * as awsx from "@pulumi/awsx";
3+
import * as eks from "@pulumi/eks";
4+
import { createArgoRole } from "./iam"
5+
import { GitOpsClusterConfig } from "./github"
6+
7+
const stackName = pulumi.getStack()
8+
const config = new pulumi.Config()
9+
let roleMappings: eks.RoleMapping[] = []
10+
11+
export const outputs: {[key: string]: any} = {
12+
"stackName": stackName,
13+
}
14+
15+
// VPC Config
16+
const vpc = new awsx.ec2.Vpc("vpc", {
17+
cidrBlock: config.require("cidrBlock"),
18+
numberOfAvailabilityZones: 3,
19+
enableDnsHostnames: true,
20+
enableDnsSupport: true,
21+
subnetSpecs: [
22+
{
23+
type: awsx.ec2.SubnetType.Private,
24+
tags: {
25+
"karpenter.sh/discovery": `${stackName}-cluster`,
26+
[`kubernetes.io/cluster/${stackName}-cluster`]: "owned",
27+
"kubernetes.io/role/internal-elb": "1",
28+
},
29+
},
30+
{
31+
type: awsx.ec2.SubnetType.Isolated,
32+
name: "tgw-attachment-subnet",
33+
cidrMask: 27,
34+
},
35+
{
36+
type: awsx.ec2.SubnetType.Public,
37+
tags: {
38+
[`kubernetes.io/cluster/${stackName}-cluster`]: "owned",
39+
"kubernetes.io/role/elb": "1",
40+
},
41+
},
42+
],
43+
})
44+
45+
// If we are creating a spoke cluster we need to create argoRole first, ensure it
46+
// gets added to auth mapping for the cluster with the correct permissions
47+
if (config.require("clusterType") === "spoke") {
48+
const argoRole = createArgoRole(config.require("awsAccountId"), pulumi.output(""), config)
49+
roleMappings.push({
50+
roleArn: argoRole.arn,
51+
username: argoRole.arn,
52+
groups: ["system:masters"],
53+
})
54+
outputs.argoRoleArn = argoRole.arn
55+
}
56+
57+
// Create EKS Cluster with a default node group
58+
const eksCluster = new eks.Cluster(`${stackName}-cluster`, {
59+
name: `${stackName}-cluster`,
60+
vpcId: vpc.vpcId,
61+
version: "1.29",
62+
publicSubnetIds: vpc.publicSubnetIds,
63+
privateSubnetIds: vpc.privateSubnetIds,
64+
roleMappings: roleMappings,
65+
nodeSecurityGroupTags: {
66+
"karpenter.sh/discovery": `${stackName}-cluster`
67+
},
68+
createOidcProvider: true,
69+
clusterTags: {
70+
"karpenter.sh/discovery": `${stackName}-cluster`,
71+
},
72+
nodeGroupOptions: {
73+
nodeSubnetIds: vpc.privateSubnetIds,
74+
nodeRootVolumeEncrypted: true,
75+
nodeRootVolumeType: "gp3",
76+
minSize: 1,
77+
maxSize: 50,
78+
desiredCapacity: 10,
79+
},
80+
})
81+
82+
outputs.clusterName = eksCluster.eksCluster.name
83+
outputs.clusterApiEndpoint = eksCluster.core.endpoint
84+
85+
const oidcProviderUrl = eksCluster.core.oidcProvider?.url as pulumi.Output<string>
86+
87+
// If we are creating the hub cluster we need pods in eks cluster to be able to assume
88+
// so we need cluster created first
89+
if (config.require("clusterType") === "hub") {
90+
const argoRole = createArgoRole(config.require("awsAccountId"), oidcProviderUrl, config)
91+
outputs.argoRoleArn = argoRole.arn
92+
}
93+
94+
// Create the GitOps Configuration for the given cluster, one method being to upload
95+
// a file to be added to Github Repo which GitOps Controller could manage or
96+
// we can create secret object and directly apply that to the cluster
97+
if (config.require("implementationType") === "github") {
98+
new GitOpsClusterConfig(outputs, config, eksCluster.eksCluster.certificateAuthority.data)
99+
} else if (config.require("implementationType") === "secret") {
100+
// TODO Implement
101+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "gitops-bridge-ts",
3+
"main": "index.ts",
4+
"devDependencies": {
5+
"@types/js-yaml": "^4.0.9",
6+
"@types/node": "^16.18.61"
7+
},
8+
"dependencies": {
9+
"@pulumi/aws": "^5.0.0",
10+
"@pulumi/awsx": "^1.0.0",
11+
"@pulumi/eks": "^2.0.0",
12+
"@pulumi/github": "^5.22.0",
13+
"@pulumi/pulumi": "^3.0.0",
14+
"process": "^0.11.10"
15+
}
16+
}

0 commit comments

Comments
 (0)