Skip to content

Commit a6ec43d

Browse files
feat(cicd): add AWS ECS deploy pipeline for dev environment
Install OIDC-based GitHub Actions workflows, CDK bootstrap assets, and .cicd configuration for us-east-1 dev deployments via ECS Fargate. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 9b1819a commit a6ec43d

20 files changed

Lines changed: 1114 additions & 0 deletions

.cicd/app.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
app_name: full-stack-fastapi-template
2+
deployment_target: ecs-fargate

.cicd/cdk/bootstrap-apprunner.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import * as cdk from "aws-cdk-lib";
2+
import * as apprunner from "aws-cdk-lib/aws-apprunner";
3+
import * as ecr from "aws-cdk-lib/aws-ecr";
4+
import * as iam from "aws-cdk-lib/aws-iam";
5+
import { Tags } from "aws-cdk-lib";
6+
7+
export interface BootstrapAppRunnerProps {
8+
appName: string;
9+
environment: string;
10+
region: string;
11+
ecrRepoName: string;
12+
}
13+
14+
export class BootstrapAppRunnerStack extends cdk.Stack {
15+
constructor(scope: cdk.App, id: string, props: BootstrapAppRunnerProps) {
16+
super(scope, id, { env: { region: props.region } });
17+
18+
Tags.of(this).add("cicd:app", props.appName);
19+
Tags.of(this).add("cicd:env", props.environment);
20+
Tags.of(this).add("cicd:managed-by", "aws-cicd-skill");
21+
22+
const repo = new ecr.Repository(this, "EcrRepo", {
23+
repositoryName: props.ecrRepoName,
24+
imageScanOnPush: true,
25+
lifecycleRules: [{ maxImageCount: 20 }],
26+
});
27+
28+
const accessRole = new iam.Role(this, "AppRunnerAccessRole", {
29+
assumedBy: new iam.ServicePrincipal("build.apprunner.amazonaws.com"),
30+
});
31+
repo.grantPull(accessRole);
32+
33+
const instanceRole = new iam.Role(this, "AppRunnerInstanceRole", {
34+
assumedBy: new iam.ServicePrincipal("tasks.apprunner.amazonaws.com"),
35+
});
36+
37+
const service = new apprunner.CfnService(this, "Service", {
38+
serviceName: `${props.appName}-${props.environment}`,
39+
sourceConfiguration: {
40+
authenticationConfiguration: {
41+
accessRoleArn: accessRole.roleArn,
42+
},
43+
imageRepository: {
44+
imageIdentifier: `${repo.repositoryUri}:latest`,
45+
imageRepositoryType: "ECR",
46+
imageConfiguration: { port: "8080" },
47+
},
48+
autoDeploymentsEnabled: false,
49+
},
50+
instanceConfiguration: {
51+
cpu: "1024",
52+
memory: "2048",
53+
instanceRoleArn: instanceRole.roleArn,
54+
},
55+
});
56+
57+
new cdk.CfnOutput(this, "EcrRepositoryUri", { value: repo.repositoryUri });
58+
new cdk.CfnOutput(this, "AppRunnerServiceArn", { value: service.attrServiceArn });
59+
new cdk.CfnOutput(this, "AppRunnerServiceUrl", { value: service.attrServiceUrl });
60+
new cdk.CfnOutput(this, "SsmHandlesHint", {
61+
value: `/cicd/${props.appName}/${props.environment}/handles`,
62+
description: "Write handles JSON here after bootstrap",
63+
});
64+
}
65+
}
66+
67+
const app = new cdk.App();
68+
const appName = app.node.tryGetContext("appName") ?? "my-app";
69+
const environment = app.node.tryGetContext("environment") ?? "dev";
70+
const region = app.node.tryGetContext("region") ?? "us-east-1";
71+
72+
new BootstrapAppRunnerStack(app, "BootstrapAppRunner", {
73+
appName,
74+
environment,
75+
region,
76+
ecrRepoName: `${appName}-${environment}`,
77+
});

.cicd/cdk/bootstrap-ecs.ts

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import * as cdk from "aws-cdk-lib";
2+
import * as ec2 from "aws-cdk-lib/aws-ec2";
3+
import * as ecs from "aws-cdk-lib/aws-ecs";
4+
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
5+
import * as ecr from "aws-cdk-lib/aws-ecr";
6+
import * as logs from "aws-cdk-lib/aws-logs";
7+
import { Tags } from "aws-cdk-lib";
8+
9+
export interface BootstrapEcsProps {
10+
appName: string;
11+
environment: string;
12+
region: string;
13+
vpcId?: string;
14+
}
15+
16+
export class BootstrapEcsStack extends cdk.Stack {
17+
constructor(scope: cdk.App, id: string, props: BootstrapEcsProps) {
18+
super(scope, id, { env: { region: props.region } });
19+
20+
Tags.of(this).add("cicd:app", props.appName);
21+
Tags.of(this).add("cicd:env", props.environment);
22+
Tags.of(this).add("cicd:managed-by", "aws-cicd-skill");
23+
24+
const vpc = props.vpcId
25+
? ec2.Vpc.fromLookup(this, "Vpc", { vpcId: props.vpcId })
26+
: new ec2.Vpc(this, "Vpc", { maxAzs: 2, natGateways: 1 });
27+
28+
const repo = new ecr.Repository(this, "EcrRepo", {
29+
repositoryName: `${props.appName}-${props.environment}`,
30+
imageScanOnPush: true,
31+
});
32+
33+
const cluster = new ecs.Cluster(this, "Cluster", {
34+
vpc,
35+
clusterName: `${props.appName}-${props.environment}`,
36+
containerInsights: true,
37+
});
38+
39+
const taskDef = new ecs.FargateTaskDefinition(this, "TaskDef", {
40+
cpu: 512,
41+
memoryLimitMiB: 1024,
42+
});
43+
taskDef.addContainer("App", {
44+
image: ecs.ContainerImage.fromEcrRepository(repo, "latest"),
45+
logging: ecs.LogDrivers.awsLogs({
46+
streamPrefix: props.appName,
47+
logRetention: logs.RetentionDays.ONE_MONTH,
48+
}),
49+
portMappings: [{ containerPort: 8080 }],
50+
});
51+
52+
const service = new ecs.FargateService(this, "Service", {
53+
cluster,
54+
taskDefinition: taskDef,
55+
desiredCount: 1,
56+
assignPublicIp: true,
57+
});
58+
59+
const alb = new elbv2.ApplicationLoadBalancer(this, "Alb", { vpc, internetFacing: true });
60+
const listener = alb.addListener("Http", { port: 80, open: true });
61+
const tg = listener.addTargets("EcsTargets", {
62+
port: 8080,
63+
targets: [service],
64+
healthCheck: { path: "/health" },
65+
});
66+
67+
new cdk.CfnOutput(this, "ClusterName", { value: cluster.clusterName });
68+
new cdk.CfnOutput(this, "TargetGroupArn", { value: tg.targetGroupArn });
69+
new cdk.CfnOutput(this, "EcrRepositoryUri", { value: repo.repositoryUri });
70+
}
71+
}
72+
73+
const app = new cdk.App();
74+
const appName = app.node.tryGetContext("appName") ?? "my-app";
75+
const environment = app.node.tryGetContext("environment") ?? "dev";
76+
const region = app.node.tryGetContext("region") ?? "us-east-1";
77+
78+
new BootstrapEcsStack(app, "BootstrapEcs", {
79+
appName,
80+
environment,
81+
region,
82+
vpcId: app.node.tryGetContext("vpcId"),
83+
});

.cicd/cdk/import-existing.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import * as cdk from "aws-cdk-lib";
2+
import * as ec2 from "aws-cdk-lib/aws-ec2";
3+
import * as ecs from "aws-cdk-lib/aws-ecs";
4+
import * as elbv2 from "aws-cdk-lib/aws-elasticloadbalancingv2";
5+
import { Tags } from "aws-cdk-lib";
6+
7+
export interface ImportExistingProps {
8+
appName: string;
9+
environment: string;
10+
region: string;
11+
clusterName: string;
12+
clusterArn: string;
13+
vpcArn: string;
14+
targetGroupArn: string;
15+
albArn: string;
16+
}
17+
18+
/** Import existing ECS/ALB resources — never creates new Cluster(). */
19+
export class ImportExistingStack extends cdk.Stack {
20+
constructor(scope: cdk.App, id: string, props: ImportExistingProps) {
21+
super(scope, id, { env: { region: props.region } });
22+
23+
Tags.of(this).add("cicd:app", props.appName);
24+
Tags.of(this).add("cicd:env", props.environment);
25+
Tags.of(this).add("cicd:managed-by", "aws-cicd-skill");
26+
27+
const vpc = ec2.Vpc.fromVpcAttributes(this, "ImportedVpc", {
28+
vpcId: app.node.tryGetContext("vpcId") ?? "vpc-placeholder",
29+
availabilityZones: ["us-east-1a", "us-east-1b"],
30+
});
31+
const cluster = ecs.Cluster.fromClusterAttributes(this, "ImportedCluster", {
32+
clusterName: props.clusterName,
33+
clusterArn: props.clusterArn,
34+
vpc,
35+
});
36+
37+
const tg = elbv2.ApplicationTargetGroup.fromTargetGroupAttributes(this, "ImportedTg", {
38+
targetGroupArn: props.targetGroupArn,
39+
});
40+
41+
elbv2.ApplicationLoadBalancer.fromApplicationLoadBalancerAttributes(this, "ImportedAlb", {
42+
loadBalancerArn: props.albArn,
43+
securityGroupId: "sg-placeholder",
44+
});
45+
46+
new cdk.CfnOutput(this, "ImportedClusterArn", { value: cluster.clusterArn });
47+
new cdk.CfnOutput(this, "ImportedTargetGroupArn", { value: tg.targetGroupArn });
48+
new cdk.CfnOutput(this, "SsmHandlesPath", {
49+
value: `/cicd/${props.appName}/${props.environment}/handles`,
50+
});
51+
}
52+
}
53+
54+
const app = new cdk.App();
55+
new ImportExistingStack(app, "ImportExisting", {
56+
appName: app.node.tryGetContext("appName") ?? "my-app",
57+
environment: app.node.tryGetContext("environment") ?? "dev",
58+
region: app.node.tryGetContext("region") ?? "us-east-1",
59+
clusterName: app.node.tryGetContext("clusterName") ?? "cluster",
60+
clusterArn: app.node.tryGetContext("clusterArn") ?? "<ARN>/cluster",
61+
vpcArn: app.node.tryGetContext("vpcArn") ?? "<ARN>/vpc",
62+
targetGroupArn: app.node.tryGetContext("targetGroupArn") ?? "<ARN>/target-group",
63+
albArn: app.node.tryGetContext("albArn") ?? "<ARN>/alb",
64+
});

.cicd/cdk/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "cicd-bootstrap-cdk",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"build": "tsc",
7+
"synth": "cdk synth"
8+
},
9+
"dependencies": {
10+
"aws-cdk-lib": "2.170.0",
11+
"constructs": "10.4.2"
12+
},
13+
"devDependencies": {
14+
"typescript": "5.6.3",
15+
"@types/node": "20.17.6"
16+
}
17+
}

.cicd/cdk/tsconfig.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"compilerOptions": {
3+
"target": "ES2022",
4+
"module": "commonjs",
5+
"lib": ["ES2022"],
6+
"strict": true,
7+
"esModuleInterop": true,
8+
"skipLibCheck": true,
9+
"outDir": "dist",
10+
"rootDir": "."
11+
},
12+
"include": ["*.ts"]
13+
}

.cicd/env/dev.yaml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
environment: dev
2+
3+
# Public metadata only — ARNs and passwords live in SSM / Secrets Manager
4+
aws:
5+
region: us-east-1
6+
# account_id: fill after `aws sts get-caller-identity`
7+
8+
app:
9+
name: full-stack-fastapi-template
10+
domain: null
11+
12+
deployment:
13+
target_override: ecs-fargate
14+
mode: single-node
15+
16+
scaling:
17+
min_instances: 1
18+
max_instances: 2
19+
20+
health:
21+
path: /api/v1/utils/health-check/
22+
timeout_seconds: 10
23+
24+
ecr:
25+
backend_repository: full-stack-fastapi-template-dev-backend
26+
frontend_repository: full-stack-fastapi-template-dev-frontend
27+
28+
secrets:
29+
database: full-stack-fastapi-template-dev/database
30+
app: full-stack-fastapi-template-dev/app
31+
32+
logging:
33+
cloudwatch:
34+
enabled: true

.cicd/env/dev.yaml.example

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
environment: dev
2+
3+
# Public metadata only — ARNs live in SSM (/cicd/<repo>/dev/handles)
4+
aws:
5+
region: us-east-1
6+
7+
app:
8+
name: my-app
9+
domain: dev.example.com
10+
11+
deployment:
12+
# Optional override; skill auto-detects apprunner vs ecs-fargate
13+
target_override: null
14+
mode: single-node
15+
16+
scaling:
17+
min_instances: 1
18+
max_instances: 2
19+
20+
health:
21+
path: /health
22+
timeout_seconds: 10

.cicd/env/prod.yaml.example

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
environment: prod
2+
3+
aws:
4+
region: us-east-1
5+
6+
app:
7+
name: my-app
8+
domain: example.com
9+
10+
deployment:
11+
target_override: null
12+
mode: distributed
13+
14+
scaling:
15+
min_instances: 2
16+
max_instances: 10
17+
18+
health:
19+
path: /health
20+
timeout_seconds: 10

.cicd/env/staging.yaml.example

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
environment: staging
2+
3+
aws:
4+
region: us-east-1
5+
6+
app:
7+
name: my-app
8+
domain: staging.example.com
9+
10+
deployment:
11+
target_override: null
12+
mode: single-node
13+
14+
scaling:
15+
min_instances: 1
16+
max_instances: 3
17+
18+
health:
19+
path: /health
20+
timeout_seconds: 10

0 commit comments

Comments
 (0)