Skip to content
Open
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
2 changes: 2 additions & 0 deletions .cicd/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
app_name: full-stack-fastapi-template
deployment_target: ecs-fargate
77 changes: 77 additions & 0 deletions .cicd/cdk/bootstrap-apprunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import * as cdk from "aws-cdk-lib";
import * as apprunner from "aws-cdk-lib/aws-apprunner";
import * as ecr from "aws-cdk-lib/aws-ecr";
import * as iam from "aws-cdk-lib/aws-iam";
import { Tags } from "aws-cdk-lib";

export interface BootstrapAppRunnerProps {
appName: string;
environment: string;
region: string;
ecrRepoName: string;
}

export class BootstrapAppRunnerStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: BootstrapAppRunnerProps) {
super(scope, id, { env: { region: props.region } });

Tags.of(this).add("cicd:app", props.appName);
Tags.of(this).add("cicd:env", props.environment);
Tags.of(this).add("cicd:managed-by", "aws-cicd-skill");

const repo = new ecr.Repository(this, "EcrRepo", {
repositoryName: props.ecrRepoName,
imageScanOnPush: true,
lifecycleRules: [{ maxImageCount: 20 }],
});

const accessRole = new iam.Role(this, "AppRunnerAccessRole", {
assumedBy: new iam.ServicePrincipal("build.apprunner.amazonaws.com"),
});
repo.grantPull(accessRole);

const instanceRole = new iam.Role(this, "AppRunnerInstanceRole", {
assumedBy: new iam.ServicePrincipal("tasks.apprunner.amazonaws.com"),
});

const service = new apprunner.CfnService(this, "Service", {
serviceName: `${props.appName}-${props.environment}`,
sourceConfiguration: {
authenticationConfiguration: {
accessRoleArn: accessRole.roleArn,
},
imageRepository: {
imageIdentifier: `${repo.repositoryUri}:latest`,
imageRepositoryType: "ECR",
imageConfiguration: { port: "8080" },
},
autoDeploymentsEnabled: false,
},
instanceConfiguration: {
cpu: "1024",
memory: "2048",
instanceRoleArn: instanceRole.roleArn,
},
});

new cdk.CfnOutput(this, "EcrRepositoryUri", { value: repo.repositoryUri });
new cdk.CfnOutput(this, "AppRunnerServiceArn", { value: service.attrServiceArn });
new cdk.CfnOutput(this, "AppRunnerServiceUrl", { value: service.attrServiceUrl });
new cdk.CfnOutput(this, "SsmHandlesHint", {
value: `/cicd/${props.appName}/${props.environment}/handles`,
description: "Write handles JSON here after bootstrap",
});
}
}

const app = new cdk.App();
const appName = app.node.tryGetContext("appName") ?? "my-app";
const environment = app.node.tryGetContext("environment") ?? "dev";
const region = app.node.tryGetContext("region") ?? "us-east-1";

new BootstrapAppRunnerStack(app, "BootstrapAppRunner", {
appName,
environment,
region,
ecrRepoName: `${appName}-${environment}`,
});
83 changes: 83 additions & 0 deletions .cicd/cdk/bootstrap-ecs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import * as ecr from "aws-cdk-lib/aws-ecr";
import * as logs from "aws-cdk-lib/aws-logs";
import { Tags } from "aws-cdk-lib";

export interface BootstrapEcsProps {
appName: string;
environment: string;
region: string;
vpcId?: string;
}

export class BootstrapEcsStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: BootstrapEcsProps) {
super(scope, id, { env: { region: props.region } });

Tags.of(this).add("cicd:app", props.appName);
Tags.of(this).add("cicd:env", props.environment);
Tags.of(this).add("cicd:managed-by", "aws-cicd-skill");

const vpc = props.vpcId
? ec2.Vpc.fromLookup(this, "Vpc", { vpcId: props.vpcId })
: new ec2.Vpc(this, "Vpc", { maxAzs: 2, natGateways: 1 });

const repo = new ecr.Repository(this, "EcrRepo", {
repositoryName: `${props.appName}-${props.environment}`,
imageScanOnPush: true,
});

const cluster = new ecs.Cluster(this, "Cluster", {
vpc,
clusterName: `${props.appName}-${props.environment}`,
containerInsights: true,
});

const taskDef = new ecs.FargateTaskDefinition(this, "TaskDef", {
cpu: 512,
memoryLimitMiB: 1024,
});
taskDef.addContainer("App", {
image: ecs.ContainerImage.fromEcrRepository(repo, "latest"),
logging: ecs.LogDrivers.awsLogs({
streamPrefix: props.appName,
logRetention: logs.RetentionDays.ONE_MONTH,
}),
portMappings: [{ containerPort: 8080 }],
});

const service = new ecs.FargateService(this, "Service", {
cluster,
taskDefinition: taskDef,
desiredCount: 1,
assignPublicIp: true,
});

const alb = new elbv2.ApplicationLoadBalancer(this, "Alb", { vpc, internetFacing: true });
const listener = alb.addListener("Http", { port: 80, open: true });
const tg = listener.addTargets("EcsTargets", {
port: 8080,
targets: [service],
healthCheck: { path: "/health" },
});

new cdk.CfnOutput(this, "ClusterName", { value: cluster.clusterName });
new cdk.CfnOutput(this, "TargetGroupArn", { value: tg.targetGroupArn });
new cdk.CfnOutput(this, "EcrRepositoryUri", { value: repo.repositoryUri });
}
}

const app = new cdk.App();
const appName = app.node.tryGetContext("appName") ?? "my-app";
const environment = app.node.tryGetContext("environment") ?? "dev";
const region = app.node.tryGetContext("region") ?? "us-east-1";

new BootstrapEcsStack(app, "BootstrapEcs", {
appName,
environment,
region,
vpcId: app.node.tryGetContext("vpcId"),
});
64 changes: 64 additions & 0 deletions .cicd/cdk/import-existing.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import * as cdk from "aws-cdk-lib";
import * as ec2 from "aws-cdk-lib/aws-ec2";
import * as ecs from "aws-cdk-lib/aws-ecs";
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
import { Tags } from "aws-cdk-lib";

export interface ImportExistingProps {
appName: string;
environment: string;
region: string;
clusterName: string;
clusterArn: string;
vpcArn: string;
targetGroupArn: string;
albArn: string;
}

/** Import existing ECS/ALB resources — never creates new Cluster(). */
export class ImportExistingStack extends cdk.Stack {
constructor(scope: cdk.App, id: string, props: ImportExistingProps) {
super(scope, id, { env: { region: props.region } });

Tags.of(this).add("cicd:app", props.appName);
Tags.of(this).add("cicd:env", props.environment);
Tags.of(this).add("cicd:managed-by", "aws-cicd-skill");

const vpc = ec2.Vpc.fromVpcAttributes(this, "ImportedVpc", {
vpcId: app.node.tryGetContext("vpcId") ?? "vpc-placeholder",
availabilityZones: ["us-east-1a", "us-east-1b"],
});
const cluster = ecs.Cluster.fromClusterAttributes(this, "ImportedCluster", {
clusterName: props.clusterName,
clusterArn: props.clusterArn,
vpc,
});

const tg = elbv2.ApplicationTargetGroup.fromTargetGroupAttributes(this, "ImportedTg", {
targetGroupArn: props.targetGroupArn,
});

elbv2.ApplicationLoadBalancer.fromApplicationLoadBalancerAttributes(this, "ImportedAlb", {
loadBalancerArn: props.albArn,
securityGroupId: "sg-placeholder",
});

new cdk.CfnOutput(this, "ImportedClusterArn", { value: cluster.clusterArn });
new cdk.CfnOutput(this, "ImportedTargetGroupArn", { value: tg.targetGroupArn });
new cdk.CfnOutput(this, "SsmHandlesPath", {
value: `/cicd/${props.appName}/${props.environment}/handles`,
});
}
}

const app = new cdk.App();
new ImportExistingStack(app, "ImportExisting", {
appName: app.node.tryGetContext("appName") ?? "my-app",
environment: app.node.tryGetContext("environment") ?? "dev",
region: app.node.tryGetContext("region") ?? "us-east-1",
clusterName: app.node.tryGetContext("clusterName") ?? "cluster",
clusterArn: app.node.tryGetContext("clusterArn") ?? "<ARN>/cluster",
vpcArn: app.node.tryGetContext("vpcArn") ?? "<ARN>/vpc",
targetGroupArn: app.node.tryGetContext("targetGroupArn") ?? "<ARN>/target-group",
albArn: app.node.tryGetContext("albArn") ?? "<ARN>/alb",
});
17 changes: 17 additions & 0 deletions .cicd/cdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "cicd-bootstrap-cdk",
"version": "1.0.0",
"private": true,
"scripts": {
"build": "tsc",
"synth": "cdk synth"
},
"dependencies": {
"aws-cdk-lib": "2.170.0",
"constructs": "10.4.2"
},
"devDependencies": {
"typescript": "5.6.3",
"@types/node": "20.17.6"
}
}
13 changes: 13 additions & 0 deletions .cicd/cdk/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "dist",
"rootDir": "."
},
"include": ["*.ts"]
}
34 changes: 34 additions & 0 deletions .cicd/env/dev.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
environment: dev

# Public metadata only — ARNs and passwords live in SSM / Secrets Manager
aws:
region: us-east-1
# account_id: fill after `aws sts get-caller-identity`

app:
name: full-stack-fastapi-template
domain: null

deployment:
target_override: ecs-fargate
mode: single-node

scaling:
min_instances: 1
max_instances: 2

health:
path: /api/v1/utils/health-check/
timeout_seconds: 10

ecr:
backend_repository: full-stack-fastapi-template-dev-backend
frontend_repository: full-stack-fastapi-template-dev-frontend

secrets:
database: full-stack-fastapi-template-dev/database
app: full-stack-fastapi-template-dev/app

logging:
cloudwatch:
enabled: true
22 changes: 22 additions & 0 deletions .cicd/env/dev.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
environment: dev

# Public metadata only — ARNs live in SSM (/cicd/<repo>/dev/handles)
aws:
region: us-east-1

app:
name: my-app
domain: dev.example.com

deployment:
# Optional override; skill auto-detects apprunner vs ecs-fargate
target_override: null
mode: single-node

scaling:
min_instances: 1
max_instances: 2

health:
path: /health
timeout_seconds: 10
20 changes: 20 additions & 0 deletions .cicd/env/prod.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
environment: prod

aws:
region: us-east-1

app:
name: my-app
domain: example.com

deployment:
target_override: null
mode: distributed

scaling:
min_instances: 2
max_instances: 10

health:
path: /health
timeout_seconds: 10
20 changes: 20 additions & 0 deletions .cicd/env/staging.yaml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
environment: staging

aws:
region: us-east-1

app:
name: my-app
domain: staging.example.com

deployment:
target_override: null
mode: single-node

scaling:
min_instances: 1
max_instances: 3

health:
path: /health
timeout_seconds: 10
Loading
Loading