Initial commit: Spicy CDK automation framework
Jenkins shared library and CDK constructs for AWS infrastructure. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
216
docs/ACCOUNTS.md
Normal file
216
docs/ACCOUNTS.md
Normal file
@@ -0,0 +1,216 @@
|
||||
# Account Configuration Guide
|
||||
|
||||
Configure AWS accounts for multi-environment deployments.
|
||||
|
||||
## Overview
|
||||
|
||||
The `accounts.groovy` utility provides a centralized way to manage AWS account configurations across environments. This eliminates hardcoding credentials and region information in individual Jenkinsfiles.
|
||||
|
||||
## Usage
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
// Get all accounts
|
||||
def allAccounts = accounts.get()
|
||||
|
||||
// Use a specific environment
|
||||
def prodAccount = allAccounts.SPICY_CA_CENRAL_1_PROD
|
||||
|
||||
spicyVPC(
|
||||
jenkinsAwsCredentialsId: prodAccount.jenkinsAwsCredentialsId,
|
||||
region: prodAccount.region,
|
||||
accountId: prodAccount.accountId,
|
||||
stackName: prodAccount.vpcStackName,
|
||||
ownerTag: "Platform",
|
||||
productTag: "spicy",
|
||||
componentTag: "vpc",
|
||||
)
|
||||
```
|
||||
|
||||
## Available Accounts
|
||||
|
||||
| Key | Environment | Description |
|
||||
|-----|-------------|-------------|
|
||||
| `SPICY_CA_CENRAL_1` | Base | Base account configuration |
|
||||
| `SPICY_CA_CENRAL_1_DEV` | Development | Development environment |
|
||||
| `SPICY_CA_CENRAL_1_SANDBOX` | Sandbox | Sandbox/experimental environment |
|
||||
| `SPICY_CA_CENRAL_1_QA` | QA | Quality assurance environment |
|
||||
| `SPICY_CA_CENRAL_1_STAGING` | Staging | Pre-production environment |
|
||||
| `SPICY_CA_CENRAL_1_PROD` | Production | Production environment |
|
||||
|
||||
## Account Properties
|
||||
|
||||
Each account object contains:
|
||||
|
||||
| Property | Type | Description |
|
||||
|----------|------|-------------|
|
||||
| `accountId` | string | AWS Account ID (12-digit) |
|
||||
| `region` | string | AWS Region (e.g., `ca-central-1`) |
|
||||
| `jenkinsAwsCredentialsId` | string | Jenkins credential ID for AWS access |
|
||||
| `vpcStackName` | string | Default VPC stack name for this environment |
|
||||
| `ecsClusterStackName` | string | Default ECS cluster stack name |
|
||||
|
||||
## Configuration Files
|
||||
|
||||
Account configurations are loaded from YAML files in the `resources/` directory:
|
||||
|
||||
```
|
||||
jenkins/
|
||||
└── resources/
|
||||
└── accounts/
|
||||
├── aws-spicy-ca-central-1.yml # Base account
|
||||
└── environments/
|
||||
├── aws-spicy-ca-central-1-dev.yml
|
||||
├── aws-spicy-ca-central-1-sandbox.yml
|
||||
├── aws-spicy-ca-central-1-staging.yml
|
||||
└── aws-spicy-ca-central-1-prod.yml
|
||||
```
|
||||
|
||||
### Base Account Configuration
|
||||
|
||||
`resources/accounts/aws-spicy-ca-central-1.yml`:
|
||||
|
||||
```yaml
|
||||
accountId: "123456789012"
|
||||
region: "ca-central-1"
|
||||
jenkinsAwsCredentialsId: "aws-spicy-ca-central-1"
|
||||
```
|
||||
|
||||
### Environment Configuration
|
||||
|
||||
`resources/accounts/environments/aws-spicy-ca-central-1-prod.yml`:
|
||||
|
||||
```yaml
|
||||
# Inherits from base account
|
||||
vpcStackName: "spicy-vpc-prod"
|
||||
ecsClusterStackName: "spicy-ecs-cluster-prod"
|
||||
jenkinsAwsCredentialsId: "aws-spicy-ca-central-1-prod"
|
||||
```
|
||||
|
||||
## Adding a New Account
|
||||
|
||||
### 1. Create Base Account File
|
||||
|
||||
Create `resources/accounts/aws-mycompany-us-west-2.yml`:
|
||||
|
||||
```yaml
|
||||
accountId: "987654321098"
|
||||
region: "us-west-2"
|
||||
jenkinsAwsCredentialsId: "aws-mycompany-us-west-2"
|
||||
```
|
||||
|
||||
### 2. Create Environment Files
|
||||
|
||||
Create `resources/accounts/environments/aws-mycompany-us-west-2-prod.yml`:
|
||||
|
||||
```yaml
|
||||
vpcStackName: "mycompany-vpc-prod"
|
||||
ecsClusterStackName: "mycompany-ecs-cluster-prod"
|
||||
```
|
||||
|
||||
### 3. Update accounts.groovy
|
||||
|
||||
Edit `jenkins/vars/accounts.groovy`:
|
||||
|
||||
```groovy
|
||||
def getAccounts() {
|
||||
def accounts = [:]
|
||||
|
||||
// Existing accounts...
|
||||
accounts.put("SPICY_CA_CENRAL_1", resources.getProperties("accounts/aws-spicy-ca-central-1.yml"))
|
||||
|
||||
// Add new account
|
||||
accounts.put("MYCOMPANY_US_WEST_2", resources.getProperties("accounts/aws-mycompany-us-west-2.yml"))
|
||||
|
||||
return accounts
|
||||
}
|
||||
|
||||
def getProduction() {
|
||||
def accounts = [:]
|
||||
|
||||
// Existing...
|
||||
accounts.put(
|
||||
"SPICY_CA_CENRAL_1_PROD",
|
||||
getAccounts().SPICY_CA_CENRAL_1 + resources.getProperties("accounts/environments/aws-spicy-ca-central-1-prod.yml")
|
||||
)
|
||||
|
||||
// Add new environment
|
||||
accounts.put(
|
||||
"MYCOMPANY_US_WEST_2_PROD",
|
||||
getAccounts().MYCOMPANY_US_WEST_2 + resources.getProperties("accounts/environments/aws-mycompany-us-west-2-prod.yml")
|
||||
)
|
||||
|
||||
return accounts
|
||||
}
|
||||
```
|
||||
|
||||
## Jenkins Credentials Setup
|
||||
|
||||
For each account, create AWS credentials in Jenkins:
|
||||
|
||||
1. Go to **Manage Jenkins** → **Manage Credentials**
|
||||
2. Add credentials:
|
||||
- **Kind**: Username with password
|
||||
- **Username**: AWS Access Key ID
|
||||
- **Password**: AWS Secret Access Key
|
||||
- **ID**: Match the `jenkinsAwsCredentialsId` in your config (e.g., `aws-spicy-ca-central-1-prod`)
|
||||
|
||||
## Multi-Region Deployments
|
||||
|
||||
Deploy to multiple regions:
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
def regions = [
|
||||
accounts.get().SPICY_CA_CENRAL_1_PROD,
|
||||
accounts.get().SPICY_EU_WEST_1_PROD, // If configured
|
||||
]
|
||||
|
||||
regions.each { account ->
|
||||
spicyVPC(
|
||||
jenkinsAwsCredentialsId: account.jenkinsAwsCredentialsId,
|
||||
region: account.region,
|
||||
stackName: "vpc-${account.region}",
|
||||
ownerTag: "Platform",
|
||||
productTag: "spicy",
|
||||
componentTag: "vpc",
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Environment-Specific Overrides
|
||||
|
||||
Override settings per environment:
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
def account = accounts.get().SPICY_CA_CENRAL_1_DEV
|
||||
|
||||
spicyVPC(
|
||||
jenkinsAwsCredentialsId: account.jenkinsAwsCredentialsId,
|
||||
region: account.region,
|
||||
stackName: account.vpcStackName,
|
||||
ownerTag: "Dev",
|
||||
productTag: "spicy",
|
||||
componentTag: "vpc",
|
||||
|
||||
// Dev-specific: fewer AZs, no NACL subnets
|
||||
numberOfAzs: 2,
|
||||
createAdditionalPrivateSubnets: false,
|
||||
)
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use environment-specific credentials**: Each environment should have its own AWS credentials with appropriate permissions.
|
||||
|
||||
2. **Naming conventions**: Use consistent naming like `{company}-{region}-{env}` for stack names.
|
||||
|
||||
3. **Least privilege**: Production credentials should only have permissions needed for deployment.
|
||||
|
||||
4. **Separate accounts**: Use separate AWS accounts for production vs non-production when possible.
|
||||
|
||||
5. **Centralize configuration**: Keep all account configs in the shared library, not in individual repos.
|
||||
925
docs/CDK_SYNTH_EXAMPLES.md
Normal file
925
docs/CDK_SYNTH_EXAMPLES.md
Normal file
@@ -0,0 +1,925 @@
|
||||
# CDK Synth Command Examples
|
||||
|
||||
This document provides comprehensive examples of `pnpm exec cdk synth` commands for all stack types and configurations.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
```bash
|
||||
cd spicy-automation
|
||||
pnpm install
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
## Common Warnings
|
||||
|
||||
The following warnings are **expected** and **non-critical**:
|
||||
|
||||
1. **Route Table ID Warnings**: When importing VPCs, route table IDs are optional. These warnings can be ignored.
|
||||
|
||||
```
|
||||
[Warning] No routeTableId was provided to the subnet 'subnet-xxx'
|
||||
```
|
||||
|
||||
2. **Construct Metadata Warnings**: CDK internal warnings about managed policies. These are informational only.
|
||||
|
||||
```
|
||||
[Warning] Failed to add construct metadata for node [ExecutionRole]
|
||||
```
|
||||
|
||||
## CloudFormation Import Pattern (Minimal Props)
|
||||
|
||||
The CDK implementation supports the same CloudFormation import pattern as the old Ansible/CloudFormation templates:
|
||||
|
||||
- **Minimal props**: Only `clusterName` required for most deployments
|
||||
- **Auto-imports**: VPC ID, listener ARNs, and logs bucket auto-import from CloudFormation exports
|
||||
- **No defaults**: All required values must exist in exports or be provided explicitly - fails early if missing
|
||||
|
||||
**Export chain:**
|
||||
|
||||
- VPC stack exports: `${vpcStackName}-VPCID`, `${vpcStackName}-VPCCIDR`, `${vpcStackName}-PrivateSubnetA1ID`, etc.
|
||||
- Cluster stack exports: `${clusterName}-VPC`, `${clusterName}-internet-facing-https-listener`, `${clusterName}-logs-s3-bucket`
|
||||
- ALB stack exports: `${albStackName}-internet-facing-arn`, `${albStackName}-internet-facing-https-listener`, `${albStackName}-internet-facing-http-listener`
|
||||
|
||||
**Important:** The implementation **fails early** if required exports don't exist. There are no defaults - either the data exists in CloudFormation exports, or the synth fails with a clear error message.
|
||||
|
||||
## VPC Stack
|
||||
|
||||
### Minimal VPC (Default Configuration)
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=vpc \
|
||||
-c stackName=my-vpc \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=vpc
|
||||
```
|
||||
|
||||
**Creates:**
|
||||
|
||||
- VPC with default CIDR (172.1.0.0/16)
|
||||
- 4 Availability Zones
|
||||
- Public subnets (one per AZ)
|
||||
- Private subnets Type 1 (one per AZ with NAT Gateways)
|
||||
- Private subnets Type 2 (one per AZ with NACLs)
|
||||
- S3 Gateway Endpoint
|
||||
|
||||
### Custom VPC Configuration
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=vpc \
|
||||
-c stackName=production-vpc \
|
||||
-c ownerTag=Platform \
|
||||
-c productTag=spicy \
|
||||
-c componentTag=network \
|
||||
-c vpcCidr=10.0.0.0/16 \
|
||||
-c numberOfAzs=3 \
|
||||
-c availabilityZones=ca-central-1a,ca-central-1b,ca-central-1c \
|
||||
-c createPrivateSubnets=true \
|
||||
-c createAdditionalPrivateSubnets=true
|
||||
```
|
||||
|
||||
### Development VPC (2 AZs, No NACL Subnets)
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=vpc \
|
||||
-c stackName=dev-vpc \
|
||||
-c ownerTag=Dev \
|
||||
-c productTag=myapp \
|
||||
-c componentTag=vpc \
|
||||
-c numberOfAzs=2 \
|
||||
-c createAdditionalPrivateSubnets=false
|
||||
```
|
||||
|
||||
### Public-Only VPC (No NAT Gateways)
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=vpc \
|
||||
-c stackName=public-vpc \
|
||||
-c ownerTag=Dev \
|
||||
-c productTag=myapp \
|
||||
-c componentTag=vpc \
|
||||
-c createPrivateSubnets=false
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ECS Cluster Stack
|
||||
|
||||
### Minimal Cluster (No Load Balancers) - Using CloudFormation Imports
|
||||
|
||||
**Minimal props approach:** Only `vpcStackName` required. All VPC details auto-import from VPC stack exports.
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-cluster \
|
||||
-c stackName=my-cluster \
|
||||
-c vpcStackName=my-vpc \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=ecs-cluster \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
**What auto-imports from VPC stack:**
|
||||
|
||||
- VPC ID from `${vpcStackName}-VPCID`
|
||||
- VPC CIDR from `${vpcStackName}-VPCCIDR`
|
||||
- Number of AZs from `${vpcStackName}-NumberOfAZs`
|
||||
- Private subnet IDs from `${vpcStackName}-PrivateSubnetA1ID`, `${vpcStackName}-PrivateSubnetB1ID`, etc.
|
||||
- Public subnet IDs from `${vpcStackName}-PublicSubnetAID`, `${vpcStackName}-PublicSubnetBID`, etc. (if `createExternalLoadBalancer=true`)
|
||||
- Availability zones auto-derived from region and number of AZs
|
||||
|
||||
### Cluster with External ALB - Using CloudFormation Imports
|
||||
|
||||
**Minimal props:**
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-cluster \
|
||||
-c stackName=my-cluster \
|
||||
-c vpcStackName=my-vpc \
|
||||
-c createExternalLoadBalancer=true \
|
||||
-c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/xxx \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=ecs-cluster \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
**What auto-imports:**
|
||||
|
||||
- All VPC details (VPC ID, CIDR, subnets, AZs) from VPC stack exports
|
||||
- Public subnets auto-imported for external ALB when `createExternalLoadBalancer=true`
|
||||
|
||||
### Cluster with Both ALBs
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-cluster \
|
||||
-c stackName=production-cluster \
|
||||
-c vpcStackName=production-vpc \
|
||||
-c createExternalLoadBalancer=true \
|
||||
-c createInternalLoadBalancer=true \
|
||||
-c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/xxx \
|
||||
-c ownerTag=Platform \
|
||||
-c productTag=spicy \
|
||||
-c componentTag=ecs-cluster \
|
||||
-c environment=prod
|
||||
```
|
||||
|
||||
### Cluster with Spot Instances
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-cluster \
|
||||
-c stackName=dev-cluster \
|
||||
-c vpcStackName=dev-vpc \
|
||||
-c spotEnabled=true \
|
||||
-c onDemandPercentage=20 \
|
||||
-c spotAllocationStrategy=capacity-optimized \
|
||||
-c instanceType=m5a.large \
|
||||
-c minClusterSize=2 \
|
||||
-c maxClusterSize=4 \
|
||||
-c ownerTag=Dev \
|
||||
-c productTag=myapp \
|
||||
-c componentTag=ecs-cluster \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
### Cluster with Fargate Enabled
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-cluster \
|
||||
-c stackName=hybrid-cluster \
|
||||
-c vpcStackName=my-vpc \
|
||||
-c enableFargate=true \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=ecs-cluster \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## ALB Stack (Persistent bg-common ALB)
|
||||
|
||||
The ALB stack creates a persistent Application Load Balancer that can be shared across multiple service deployments. This is the "bg-common" pattern where the ALB persists even when service stacks are deleted and recreated.
|
||||
|
||||
**Important:** The ALB stack must be deployed **before** service stacks that reference it. The ALB ARN and listener ARNs from the ALB stack outputs are required as inputs to the service stack.
|
||||
|
||||
### Internet-Facing ALB
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=alb \
|
||||
-c stackName=my-service-alb \
|
||||
-c vpcId=vpc-12345678 \
|
||||
-c vpcCidrBlock=10.0.0.0/16 \
|
||||
-c availabilityZones=ca-central-1a,ca-central-1b \
|
||||
-c subnetIds=subnet-xxx,subnet-yyy \
|
||||
-c scheme=internet-facing \
|
||||
-c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/xxx \
|
||||
-c logsBucketName=my-cluster-logs \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=alb \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
**Creates:**
|
||||
|
||||
- Application Load Balancer (internet-facing)
|
||||
- Security Group for ALB
|
||||
- HTTP Listener (port 80)
|
||||
- HTTPS Listener (port 443) with SSL certificate
|
||||
- HTTP→HTTPS redirect rule
|
||||
- ALB access logs to S3 (if logsBucketName provided)
|
||||
|
||||
### Internal ALB
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=alb \
|
||||
-c stackName=my-service-alb \
|
||||
-c vpcId=vpc-12345678 \
|
||||
-c vpcCidrBlock=10.0.0.0/16 \
|
||||
-c availabilityZones=ca-central-1a,ca-central-1b \
|
||||
-c subnetIds=subnet-aaa,subnet-bbb \
|
||||
-c scheme=internal \
|
||||
-c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/xxx \
|
||||
-c logsBucketName=my-cluster-logs \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=alb \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
### ALB with Blue/Green DNS
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=alb \
|
||||
-c stackName=my-service-alb \
|
||||
-c vpcId=vpc-12345678 \
|
||||
-c vpcCidrBlock=10.0.0.0/16 \
|
||||
-c availabilityZones=ca-central-1a,ca-central-1b \
|
||||
-c subnetIds=subnet-xxx,subnet-yyy \
|
||||
-c scheme=internet-facing \
|
||||
-c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/xxx \
|
||||
-c logsBucketName=my-cluster-logs \
|
||||
-c activeHostname=api.example.com \
|
||||
-c inactiveHostname=inactive-api.example.com \
|
||||
-c bgHostedZoneId=Z1234567890 \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=alb \
|
||||
-c environment=prod
|
||||
```
|
||||
|
||||
**Creates:**
|
||||
|
||||
- All resources from internet-facing ALB example
|
||||
- Route53 A record for active hostname (api.example.com)
|
||||
- Route53 A record for inactive hostname (inactive-api.example.com)
|
||||
|
||||
---
|
||||
|
||||
## ECS Service Stack
|
||||
|
||||
### Minimal Service (No Load Balancer) - Using CloudFormation Imports
|
||||
|
||||
**Minimal props approach:** Only `clusterName` is required. VPC info auto-imports from cluster stack exports.
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-service \
|
||||
-c stackName=my-service \
|
||||
-c clusterName=my-cluster \
|
||||
-c serviceName=my-service \
|
||||
-c image=nginx:latest \
|
||||
-c containerPort=80 \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=service \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
**What auto-imports:**
|
||||
|
||||
- VPC stack name from `${clusterStackName}-VPCStackName` (cluster stack export)
|
||||
- VPC ID from `${clusterStackName}-VPC` (cluster stack export)
|
||||
- VPC CIDR from `${vpcStackName}-VPCCIDR` (VPC stack export, using imported VPC stack name)
|
||||
- Private subnets from `${vpcStackName}-PrivateSubnetA1ID`, etc. (VPC stack exports)
|
||||
- Number of AZs from `${vpcStackName}-NumberOfAZs` (VPC stack export)
|
||||
- Logs bucket from `${clusterStackName}-logs-s3-bucket` (cluster stack export)
|
||||
|
||||
**Note:** The service stack imports the VPC stack name from the cluster stack export `${clusterStackName}-VPCStackName`, then uses that to import all VPC details (CIDR, subnets, AZs) from the VPC stack exports.
|
||||
|
||||
### Service with Cluster ALB - Using CloudFormation Imports
|
||||
|
||||
**Minimal props:** Listener ARN auto-imports from cluster stack.
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-service \
|
||||
-c stackName=my-service \
|
||||
-c clusterName=my-cluster \
|
||||
-c serviceName=my-service \
|
||||
-c image=nginx:latest \
|
||||
-c containerPort=80 \
|
||||
-c hostHeader=api.example.com \
|
||||
-c priority=100 \
|
||||
-c useExternalALB=true \
|
||||
-c healthCheckPath=/health \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=service \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
**What auto-imports:**
|
||||
|
||||
- VPC stack name from `${clusterStackName}-VPCStackName` (cluster stack export)
|
||||
- VPC ID from `${clusterStackName}-VPC` (cluster stack export)
|
||||
- VPC CIDR from `${vpcStackName}-VPCCIDR` (VPC stack export, using imported VPC stack name)
|
||||
- Private subnets from `${vpcStackName}-PrivateSubnetA1ID`, etc. (VPC stack exports)
|
||||
- Number of AZs from `${vpcStackName}-NumberOfAZs` (VPC stack export)
|
||||
- External listener ARN from `${clusterStackName}-internet-facing-https-listener` (when `useExternalALB=true`)
|
||||
- Internal listener ARN from `${clusterStackName}-internal-https-listener` (when `useInternalALB=true`)
|
||||
|
||||
**Explicit listener ARN (backward compatible):**
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-service \
|
||||
-c stackName=my-service \
|
||||
-c clusterName=my-cluster \
|
||||
-c vpcId=vpc-12345678 \
|
||||
-c vpcCidrBlock=10.0.0.0/16 \
|
||||
-c availabilityZones=ca-central-1a,ca-central-1b \
|
||||
-c privateSubnetIds=subnet-aaa,subnet-bbb \
|
||||
-c serviceName=my-service \
|
||||
-c image=nginx:latest \
|
||||
-c containerPort=80 \
|
||||
-c hostHeader=api.example.com \
|
||||
-c priority=100 \
|
||||
-c useExternalALB=true \
|
||||
-c externalListenerArn=arn:aws:elasticloadbalancing:ca-central-1:123456789:listener/app/test-alb/1234567890123456/1234567890123456 \
|
||||
-c healthCheckPath=/health \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=service \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
### Service with bg-common ALB (Default - useClusterAlb=false)
|
||||
|
||||
**Important:** When using bg-common ALB, the ALB and Route53 DNS records are created in a **separate ALB stack**, not in the service stack. The service stack only creates listener rules on the existing ALB.
|
||||
|
||||
**Step 1: Deploy ALB Stack** (creates ALB and Route53 records)
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=alb \
|
||||
-c stackName=my-service-alb \
|
||||
-c vpcId=vpc-12345678 \
|
||||
-c vpcCidrBlock=10.0.0.0/16 \
|
||||
-c availabilityZones=ca-central-1a,ca-central-1b \
|
||||
-c subnetIds=subnet-xxx,subnet-yyy \
|
||||
-c scheme=internet-facing \
|
||||
-c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/xxx \
|
||||
-c logsBucketName=my-cluster-logs \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=alb \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
**Step 2: Deploy Service Stack** (creates listener rules on existing ALB)
|
||||
|
||||
**Minimal props using CloudFormation imports:**
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-service \
|
||||
-c stackName=my-service \
|
||||
-c clusterName=my-cluster \
|
||||
-c albStackName=my-service-alb \
|
||||
-c serviceName=my-service \
|
||||
-c image=nginx:latest \
|
||||
-c containerPort=80 \
|
||||
-c healthCheckPath=/health \
|
||||
-c hostHeader=api.example.com \
|
||||
-c priority=100 \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=service \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
**What auto-imports:**
|
||||
|
||||
- VPC ID from `${clusterName}-VPC` (cluster stack export)
|
||||
- VPC CIDR from `${clusterName}-VPCCIDR` (cluster stack export)
|
||||
- Private subnets from `${clusterName}-PrivateSubnetA1ID`, etc. (cluster stack exports)
|
||||
- Availability zones from `${clusterName}-AvailabilityZones` (cluster stack export)
|
||||
- ALB ARN from `${albStackName}-internet-facing-arn` (ALB stack export)
|
||||
- HTTPS listener ARN from `${albStackName}-internet-facing-https-listener` (ALB stack export)
|
||||
- HTTP listener ARN from `${albStackName}-internet-facing-http-listener` (ALB stack export)
|
||||
- Logs bucket from `${clusterName}-logs-s3-bucket` (cluster stack export)
|
||||
|
||||
**Note:** `vpcStackName` is optional - the service stack prefers importing from the cluster stack. The ALB stack name (`albStackName`) is automatically derived from the service stackName (e.g., `my-service-dev` → `my-service-dev-alb`).
|
||||
|
||||
**Note:** The service creates listener rules on **both** HTTP and HTTPS listeners (like the old template), so both listeners are imported if available.
|
||||
|
||||
**Note:** In the Jenkins pipeline, the ALB stack is created automatically. For manual CDK synth, you must deploy the ALB stack first and use `albStackName` for auto-imports.
|
||||
|
||||
### Service with bg-common ALB (Internal)
|
||||
|
||||
**Step 1: Deploy Internal ALB Stack**
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=alb \
|
||||
-c stackName=my-service-alb \
|
||||
-c vpcId=vpc-12345678 \
|
||||
-c vpcCidrBlock=10.0.0.0/16 \
|
||||
-c availabilityZones=ca-central-1a,ca-central-1b \
|
||||
-c subnetIds=subnet-aaa,subnet-bbb \
|
||||
-c scheme=internal \
|
||||
-c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/xxx \
|
||||
-c logsBucketName=my-cluster-logs \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=alb \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
**Step 2: Deploy Service Stack**
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-service \
|
||||
-c stackName=my-service \
|
||||
-c clusterName=my-cluster \
|
||||
-c albStackName=my-service-alb \
|
||||
-c serviceName=my-service \
|
||||
-c image=nginx:latest \
|
||||
-c containerPort=80 \
|
||||
-c healthCheckPath=/health \
|
||||
-c hostHeader=api-internal.example.com \
|
||||
-c priority=100 \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=service \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
### Service with bg-common ALB + Blue/Green Deployment
|
||||
|
||||
**Note:** Blue/green deployments are handled automatically by the Jenkins pipeline. The pipeline:
|
||||
|
||||
1. Automatically creates the bg-common ALB stack (if it doesn't exist)
|
||||
2. Deploys to the inactive color (blue or green)
|
||||
3. Runs `blueGreenTest` against the inactive hostname
|
||||
4. Waits for deployment to stabilize
|
||||
5. Swaps hostnames to make the new deployment active
|
||||
6. Runs `smokeTest` against the new active hostname
|
||||
7. Keeps the old stack for rollback window (default 2 hours)
|
||||
|
||||
**For manual CDK synth, deploy both stacks:**
|
||||
|
||||
**Step 1: Deploy ALB Stack with Blue/Green DNS**
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=alb \
|
||||
-c stackName=my-service-alb \
|
||||
-c vpcId=vpc-12345678 \
|
||||
-c vpcCidrBlock=10.0.0.0/16 \
|
||||
-c availabilityZones=ca-central-1a,ca-central-1b \
|
||||
-c subnetIds=subnet-xxx,subnet-yyy \
|
||||
-c scheme=internet-facing \
|
||||
-c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/xxx \
|
||||
-c logsBucketName=my-cluster-logs \
|
||||
-c activeHostname=api.example.com \
|
||||
-c inactiveHostname=inactive-api.example.com \
|
||||
-c bgHostedZoneId=Z1234567890 \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=alb \
|
||||
-c environment=prod
|
||||
```
|
||||
|
||||
**Step 2: Deploy Service Stack (Blue or Green)**
|
||||
|
||||
After deploying the ALB stack, use `albStackName` for auto-imports:
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-service \
|
||||
-c stackName=my-service-blue \
|
||||
-c clusterName=my-cluster \
|
||||
-c albStackName=my-service-alb \
|
||||
-c serviceName=my-service \
|
||||
-c image=nginx:latest \
|
||||
-c containerPort=80 \
|
||||
-c activeHostname=api.example.com \
|
||||
-c inactiveHostname=inactive-api.example.com \
|
||||
-c bgHostedZoneId=Z1234567890 \
|
||||
-c healthCheckPath=/health \
|
||||
-c hostHeader=api.example.com \
|
||||
-c priority=100 \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=service \
|
||||
-c environment=prod
|
||||
```
|
||||
|
||||
**How blue/green works:**
|
||||
|
||||
1. ALB stack creates DNS records for both active and inactive hostnames (both point to the same ALB)
|
||||
2. Pipeline determines current active color from SSM Parameter Store (defaults to 'blue')
|
||||
3. Pipeline deploys new version to inactive color (green if blue is active, blue if green is active)
|
||||
4. Pipeline runs `blueGreenTest` hook against inactive hostname
|
||||
5. Pipeline swaps hostnames by updating listener rule priorities
|
||||
6. Pipeline runs `smokeTest` hook against new active hostname
|
||||
7. Old stack is retained for rollback window (configurable via `rollbackWindowHours`)
|
||||
8. Rollback is instant - just swaps hostnames back (no new deployment needed)
|
||||
|
||||
### Service with Auto-Scaling
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-service \
|
||||
-c stackName=my-service \
|
||||
-c clusterName=my-cluster \
|
||||
-c serviceName=my-service \
|
||||
-c image=nginx:latest \
|
||||
-c containerPort=80 \
|
||||
-c desiredCount=2 \
|
||||
-c minCapacity=2 \
|
||||
-c maxCapacity=20 \
|
||||
-c targetCpuUtilization=70 \
|
||||
-c targetMemoryUtilization=80 \
|
||||
-c targetRequestsPerTarget=1000 \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=service \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
### Service with Environment Variables and Secrets
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-service \
|
||||
-c stackName=my-service \
|
||||
-c clusterName=my-cluster \
|
||||
-c serviceName=my-service \
|
||||
-c image=nginx:latest \
|
||||
-c containerPort=80 \
|
||||
-c environment_vars='{"NODE_ENV":"production","LOG_LEVEL":"info"}' \
|
||||
-c secrets='{"DATABASE_URL":"arn:aws:secretsmanager:ca-central-1:123456789:secret:db-url","API_KEY":"arn:aws:secretsmanager:ca-central-1:123456789:secret:api-key"}' \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=service \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
### Service with Mixed Capacity Strategy (EC2 + Fargate)
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-service \
|
||||
-c stackName=my-service \
|
||||
-c clusterName=my-cluster \
|
||||
-c serviceName=my-service \
|
||||
-c image=nginx:latest \
|
||||
-c containerPort=80 \
|
||||
-c cpu=512 \
|
||||
-c memory=1024 \
|
||||
-c capacityProviderStrategy='[{"capacityProvider":"my-cluster-ec2","base":2,"weight":3},{"capacityProvider":"FARGATE_SPOT","weight":1}]' \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=service \
|
||||
-c environment=dev
|
||||
```
|
||||
|
||||
### Service with Custom Resource Limits
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-service \
|
||||
-c stackName=my-service \
|
||||
-c clusterName=my-cluster \
|
||||
-c serviceName=my-service \
|
||||
-c image=nginx:latest \
|
||||
-c containerPort=3000 \
|
||||
-c cpu=1024 \
|
||||
-c memory=2048 \
|
||||
-c desiredCount=4 \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myproduct \
|
||||
-c componentTag=service \
|
||||
-c environment=prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Production Example
|
||||
|
||||
### Full Stack Deployment Sequence
|
||||
|
||||
```bash
|
||||
# 1. Deploy VPC
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=vpc \
|
||||
-c stackName=production-vpc \
|
||||
-c ownerTag=Platform \
|
||||
-c productTag=spicy \
|
||||
-c componentTag=vpc \
|
||||
-c numberOfAzs=4 \
|
||||
-c availabilityZones=ca-central-1a,ca-central-1b,ca-central-1c,ca-central-1d
|
||||
|
||||
# 2. Deploy ECS Cluster
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-cluster \
|
||||
-c stackName=production-cluster \
|
||||
-c vpcStackName=production-vpc \
|
||||
-c createExternalLoadBalancer=true \
|
||||
-c createInternalLoadBalancer=true \
|
||||
-c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/prod \
|
||||
-c instanceType=m5a.xlarge \
|
||||
-c minClusterSize=3 \
|
||||
-c maxClusterSize=10 \
|
||||
-c spotEnabled=true \
|
||||
-c onDemandPercentage=50 \
|
||||
-c ownerTag=Platform \
|
||||
-c productTag=spicy \
|
||||
-c componentTag=ecs-cluster \
|
||||
-c environment=prod
|
||||
|
||||
# 3. Deploy ALB Stack (bg-common pattern)
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=alb \
|
||||
-c stackName=api-service-prod-alb \
|
||||
-c vpcStackName=production-vpc \
|
||||
-c numberOfAzs=3 \
|
||||
-c scheme=internet-facing \
|
||||
-c certificateArn=arn:aws:acm:ca-central-1:123456789:certificate/prod \
|
||||
-c logsBucketName=production-cluster-ca-central-1-alb-logs \
|
||||
-c activeHostname=api.example.com \
|
||||
-c inactiveHostname=inactive-api.example.com \
|
||||
-c bgHostedZoneId=Z1234567890 \
|
||||
-c ownerTag=Platform \
|
||||
-c productTag=spicy \
|
||||
-c componentTag=alb \
|
||||
-c environment=prod
|
||||
|
||||
# 4. Deploy Service Stack (references existing ALB via albStackName)
|
||||
pnpm exec cdk synth \
|
||||
-c stackType=ecs-service \
|
||||
-c stackName=api-service-prod-blue \
|
||||
-c clusterName=production-cluster \
|
||||
-c albStackName=api-service-prod-alb \
|
||||
-c serviceName=api-service \
|
||||
-c image=nexus.kodeniks.com/docker-hosted/api-service:v1.2.3 \
|
||||
-c containerPort=3000 \
|
||||
-c cpu=512 \
|
||||
-c memory=1024 \
|
||||
-c activeHostname=api.example.com \
|
||||
-c inactiveHostname=inactive-api.example.com \
|
||||
-c bgHostedZoneId=Z1234567890 \
|
||||
-c desiredCount=4 \
|
||||
-c minCapacity=4 \
|
||||
-c maxCapacity=24 \
|
||||
-c targetCpuUtilization=70 \
|
||||
-c targetMemoryUtilization=80 \
|
||||
-c targetRequestsPerTarget=1000 \
|
||||
-c healthCheckPath=/health \
|
||||
-c hostHeader=api.example.com \
|
||||
-c priority=100 \
|
||||
-c ownerTag=Platform \
|
||||
-c productTag=spicy \
|
||||
-c componentTag=api \
|
||||
-c environment=prod
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Parameter Reference
|
||||
|
||||
### Common Parameters (All Stacks)
|
||||
|
||||
| Parameter | Required | Description | Example |
|
||||
| -------------- | -------- | ----------------------------------------------- | ----------- |
|
||||
| `stackType` | ✅ | Stack type: `vpc`, `ecs-cluster`, `ecs-service` | `vpc` |
|
||||
| `stackName` | ✅ | CloudFormation stack name | `my-vpc` |
|
||||
| `ownerTag` | ✅ | Owner tag | `MyTeam` |
|
||||
| `productTag` | ✅ | Product tag | `myproduct` |
|
||||
| `componentTag` | ✅ | Component tag | `vpc` |
|
||||
| `environment` | ⚠️ | Environment (required for cluster/service) | `dev` |
|
||||
|
||||
### VPC-Specific Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| -------------------------------- | -------------- | ----------------------------- |
|
||||
| `vpcCidr` | `172.1.0.0/16` | VPC CIDR block |
|
||||
| `numberOfAzs` | `4` | Number of AZs (2, 3, or 4) |
|
||||
| `availabilityZones` | auto | Comma-separated AZ list |
|
||||
| `createPrivateSubnets` | `true` | Create private subnets |
|
||||
| `createAdditionalPrivateSubnets` | `true` | Create NACL-protected subnets |
|
||||
|
||||
### ECS Cluster Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ---------------------------- | --------------- | ---------------------------------------------------------------------------------------------------- |
|
||||
| `vpcStackName` | ✅ **Required** | VPC stack name - **all VPC details** (VPC ID, CIDR, subnets, AZs) auto-import from VPC stack exports |
|
||||
| `instanceType` | `m5a.large` | EC2 instance type |
|
||||
| `minClusterSize` | `2` | Minimum instances |
|
||||
| `maxClusterSize` | `4` | Maximum instances |
|
||||
| `createExternalLoadBalancer` | `false` | Create external ALB (public subnets auto-imported from VPC stack if enabled) |
|
||||
| `createInternalLoadBalancer` | `false` | Create internal ALB |
|
||||
| `certificateArn` | - | SSL certificate ARN |
|
||||
| `spotEnabled` | `false` | Enable Spot instances |
|
||||
| `onDemandPercentage` | `100` | On-demand percentage |
|
||||
| `enableFargate` | `false` | Enable Fargate providers |
|
||||
|
||||
### ALB Stack Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| --------------------- | ----------------- | -------------------------------------------------------------------------------------------------------- |
|
||||
| `vpcId` | - | VPC ID (required, or use `vpcStackName` to import) |
|
||||
| `vpcCidrBlock` | - | VPC CIDR (required, or use `vpcStackName` to import) |
|
||||
| `availabilityZones` | - | Comma-separated AZs (required, or use `vpcStackName` + `numberOfAzs` to import) |
|
||||
| `subnetIds` | - | Subnet IDs for ALB (required) |
|
||||
| `scheme` | `internet-facing` | ALB scheme: `internet-facing` or `internal` |
|
||||
| `certificateArn` | - | SSL certificate ARN (required for HTTPS) |
|
||||
| `logsBucketName` | - | S3 bucket name for ALB access logs |
|
||||
| `logsPrefix` | - | S3 prefix for ALB access logs |
|
||||
| `idleTimeout` | `60` | ALB idle timeout in seconds |
|
||||
| `redirectHttpToHttps` | `true` | Enable HTTP→HTTPS redirect |
|
||||
| `hostName` | - | Simple hostname (e.g., api.example.com) - auto-generates active/inactive hostnames (like old `HostName`) |
|
||||
| `activeHostname` | - | Active hostname for blue/green DNS |
|
||||
| `inactiveHostname` | - | Inactive hostname for blue/green DNS |
|
||||
| `bgHostedZoneId` | - | Route53 hosted zone ID for blue/green DNS (optional - only needed if creating DNS records) |
|
||||
|
||||
### ECS Service Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `clusterName` | - | ECS cluster name (required) - used to auto-import VPC stack name, VPC ID, and listener ARNs from cluster stack exports |
|
||||
| `clusterStackName` | - | Cluster stack name (defaults to `clusterName`) - used for CloudFormation imports |
|
||||
| `vpcStackName` | - | VPC stack name (auto-imported from `${clusterStackName}-VPCStackName`, or provide explicitly) |
|
||||
| `albStackName` | - | ALB stack name (for bg-common ALB) - used to auto-import ALB ARN and listener ARNs from ALB stack |
|
||||
| `vpcId` | - | VPC ID (required, or auto-imported from `${clusterStackName}-VPC`) |
|
||||
| `vpcCidrBlock` | - | VPC CIDR (required, or auto-imported from `${vpcStackName}-VPCCIDR` using imported VPC stack name) |
|
||||
| `availabilityZones` | - | Comma-separated AZs (required, or auto-derived from VPC stack imports with `numberOfAzs`) |
|
||||
| `privateSubnetIds` | - | Private subnet IDs (required, or auto-imported from `${vpcStackName}-PrivateSubnetA1ID`, etc.) |
|
||||
| `numberOfAzs` | - | Number of AZs (required when importing subnets from VPC stack) |
|
||||
| `publicSubnetIds` | - | Public subnet IDs (for individual ALB) |
|
||||
| `serviceName` | - | Service name (required) |
|
||||
| `image` | - | Docker image URI (required) |
|
||||
| `containerPort` | `3000` | Container port |
|
||||
| `cpu` | `256` | CPU units |
|
||||
| `memory` | `512` | Memory in MiB |
|
||||
| `desiredCount` | `2` | Desired task count |
|
||||
|
||||
#### bg-common ALB Parameters (useClusterAlb=false)
|
||||
|
||||
When `useClusterAlb` is false or not set, the service uses a bg-common ALB from a separate ALB stack. These parameters reference the existing ALB:
|
||||
|
||||
**Minimal props using CloudFormation imports:**
|
||||
|
||||
- Provide `albStackName` to auto-import ALB ARN and listener ARNs
|
||||
- Service creates listener rules on **both** HTTP and HTTPS listeners (like the old template)
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ----------------------------- | ------- | --------------------------------------------------------------------------------------------------------------- |
|
||||
| `albStackName` | - | ALB stack name - used to auto-import ALB ARN and listener ARNs (preferred) |
|
||||
| `existingAlbArn` | - | ALB ARN (auto-imported from `${albStackName}-internet-facing-arn`, or provide explicitly) |
|
||||
| `existingAlbHttpsListenerArn` | - | HTTPS listener ARN (auto-imported from `${albStackName}-internet-facing-https-listener`, or provide explicitly) |
|
||||
| `existingAlbHttpListenerArn` | - | HTTP listener ARN (auto-imported from `${albStackName}-internet-facing-http-listener`, or provide explicitly) |
|
||||
| `hostHeader` | - | Host header for listener rule |
|
||||
| `priority` | `100` | Listener rule priority (lower = higher precedence) |
|
||||
| `stickiness` | `true` | Enable session stickiness on target group |
|
||||
| `stickinessDuration` | `86400` | Session stickiness duration in seconds |
|
||||
| `deregistrationDelay` | `60` | Target deregistration delay in seconds |
|
||||
| `hostName` | - | Simple hostname (e.g., api.example.com) - auto-generates active/inactive (like old `HostName`) |
|
||||
| `activeHostname` | - | Active hostname for blue/green DNS (must match ALB stack config) |
|
||||
| `inactiveHostname` | - | Inactive hostname for blue/green DNS (must match ALB stack config) |
|
||||
| `bgHostedZoneId` | - | Route53 hosted zone ID (optional - only needed if creating DNS records) |
|
||||
|
||||
#### Blue/Green Deployment Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| --------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------- |
|
||||
| `blueGreen` | `false` | Enable blue/green deployment (pipeline handles color switching automatically) |
|
||||
| `hostName` | - | Simple hostname (e.g., api.example.com) - auto-generates `activeHostname` and `inactiveHostname` (like old `HostName`) |
|
||||
| `activeHostname` | - | Active hostname (e.g., api.example.com) - used by bg-common ALB stack to create DNS records |
|
||||
| `inactiveHostname` | - | Inactive hostname (e.g., inactive-api.example.com) - used by bg-common ALB stack to create DNS records |
|
||||
| `bgHostedZoneId` | - | Route53 hosted zone ID (for bg-common ALB stack DNS records) - required for DNS records (hostnames are optional) |
|
||||
| `rollbackWindowHours` | `2` | Hours to keep old stack for rollback (default 2 hours) |
|
||||
|
||||
**Note:** Hostnames are **optional**. Services without hostnames (pub/sub workers, background jobs) don't need DNS records. Only provide `bgHostedZoneId` if you want Route53 DNS records created.
|
||||
|
||||
**Cloudflare DNS Delegation:** If using Cloudflare for DNS, you can delegate a subdomain to AWS Route53:
|
||||
|
||||
1. Create a Route53 hosted zone for the subdomain (e.g., `production.mydomain.com`)
|
||||
2. Point Cloudflare NS records for `*.production.mydomain.com` to the Route53 name servers
|
||||
3. Use the Route53 hosted zone ID as `bgHostedZoneId`
|
||||
4. AWS will manage DNS records for all services under that subdomain
|
||||
|
||||
#### Cluster ALB Parameters (useClusterAlb=true)
|
||||
|
||||
When `useClusterAlb` is true, the service uses the cluster's shared ALB:
|
||||
|
||||
**Minimal props using CloudFormation imports:**
|
||||
|
||||
- Provide `clusterName` to auto-import listener ARNs from cluster stack exports
|
||||
- Listener ARNs auto-import from `${clusterName}-internet-facing-https-listener` or `${clusterName}-internal-https-listener`
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| --------------------- | ------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `useClusterAlb` | `false` | Set to `true` to use cluster ALB |
|
||||
| `hostHeader` | - | Host header for routing |
|
||||
| `pathPatterns` | - | Comma-separated path patterns |
|
||||
| `priority` | `100` | Listener rule priority |
|
||||
| `useExternalALB` | `false` | Use external cluster ALB (auto-imports listener ARN from `${clusterName}-internet-facing-https-listener`) |
|
||||
| `useInternalALB` | `false` | Use internal cluster ALB (auto-imports listener ARN from `${clusterName}-internal-https-listener`) |
|
||||
| `externalListenerArn` | - | External listener ARN (auto-imported when `useExternalALB=true`, or provide explicitly) |
|
||||
| `internalListenerArn` | - | Internal listener ARN (auto-imported when `useInternalALB=true`, or provide explicitly) |
|
||||
|
||||
#### Auto-Scaling Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------------------- | ------- | ----------------------------------- |
|
||||
| `minCapacity` | - | Minimum tasks |
|
||||
| `maxCapacity` | - | Maximum tasks |
|
||||
| `targetCpuUtilization` | - | Target CPU % (0-100) |
|
||||
| `targetMemoryUtilization` | - | Target memory % (0-100) |
|
||||
| `targetRequestsPerTarget` | - | Target requests per task per minute |
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
When running synth commands, verify:
|
||||
|
||||
- ✅ No errors (only expected warnings)
|
||||
- ✅ Resources are created as expected
|
||||
- ✅ Logical IDs match expected patterns
|
||||
- ✅ Tags are applied correctly
|
||||
- ✅ Outputs are exported correctly
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Missing required parameters**: Check error message for missing context values
|
||||
2. **Invalid subnet IDs**: Ensure subnet IDs exist in the specified VPC
|
||||
3. **Certificate ARN format**: Must be valid ACM certificate ARN
|
||||
4. **JSON parsing errors**: Ensure JSON strings are properly escaped/quoted
|
||||
|
||||
### Getting Stack Outputs
|
||||
|
||||
After deployment, get stack outputs:
|
||||
|
||||
```bash
|
||||
aws cloudformation describe-stacks \
|
||||
--stack-name my-stack \
|
||||
--query 'Stacks[0].Outputs' \
|
||||
--output table
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
### Minimal Commands
|
||||
|
||||
**VPC:**
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth -c stackType=vpc -c stackName=test -c ownerTag=T -c productTag=t -c componentTag=v
|
||||
```
|
||||
|
||||
**Cluster:**
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth -c stackType=ecs-cluster -c stackName=test -c vpcStackName=test-vpc -c ownerTag=T -c productTag=t -c componentTag=c -c environment=d
|
||||
```
|
||||
|
||||
**Service:**
|
||||
|
||||
```bash
|
||||
pnpm exec cdk synth -c stackType=ecs-service -c stackName=test -c clusterName=test -c vpcId=vpc-123 -c vpcCidrBlock=10.0.0.0/16 -c availabilityZones=ca-central-1a -c privateSubnetIds=subnet-123 -c serviceName=test -c image=nginx:latest -c containerPort=80 -c ownerTag=T -c productTag=t -c componentTag=s -c environment=d
|
||||
```
|
||||
292
docs/COST_OPTIMIZATION_TESTING.md
Normal file
292
docs/COST_OPTIMIZATION_TESTING.md
Normal file
@@ -0,0 +1,292 @@
|
||||
# Cost Optimization for Testing Environments
|
||||
|
||||
Barebones setup guide for VPC → Cluster → Service with blue/green deployment and internet-facing ALB.
|
||||
|
||||
## CloudFormation Import Pattern
|
||||
|
||||
All examples use the **CloudFormation import pattern** for minimal configuration:
|
||||
|
||||
- **VPC Stack**: Only `numberOfAzs` needs to be specified (exports `${vpcStackName}-NumberOfAZs`)
|
||||
- **Cluster Stack**: Uses `vpcStackName` to auto-import VPC ID, CIDR, subnets, and `numberOfAzs`. Exports `${clusterStackName}-VPCStackName` for other stacks.
|
||||
- **Service Stack**: Uses `clusterName` to import VPC stack name from cluster export, then imports VPC details from VPC stack
|
||||
- **ALB Stack**: Uses `clusterName` to import VPC stack name from cluster export, then imports VPC details from VPC stack
|
||||
|
||||
**Key benefit**: `numberOfAzs` only needs to be specified once (in the VPC stack), and all other stacks automatically import it.
|
||||
|
||||
## Cost Breakdown
|
||||
|
||||
### Optimized Testing Setup
|
||||
|
||||
- **VPC**: 2 AZs, 2 NAT Gateways = ~$64/month (NAT) + ~$2/month (EIPs)
|
||||
- **Cluster**: 1 x t3.small instance (Spot) = ~$5-8/month, OR Fargate-only (0 instances) = $0/month
|
||||
- **ALB**: Individual ALB per service = ~$16/month
|
||||
- **Service**: 1 task = minimal additional cost
|
||||
- **Total**: ~$87-90/month (with EC2) or ~$82-84/month (Fargate-only)
|
||||
|
||||
**Savings:** ~70-80% reduction vs production setup
|
||||
|
||||
## Barebones Blue/Green Setup
|
||||
|
||||
### Complete Configuration
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
// 1. VPC (2 AZs, minimal cost)
|
||||
spicyVPC(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "test-vpc",
|
||||
ownerTag: "Test",
|
||||
productTag: "test",
|
||||
componentTag: "vpc",
|
||||
numberOfAzs: 2, // 2 AZs instead of 4 (saves 2 NAT Gateways)
|
||||
createAdditionalPrivateSubnets: false, // Skip NACL subnets
|
||||
)
|
||||
|
||||
// 2. Cluster (with Fargate enabled for cheaper option)
|
||||
spicyECSCluster(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "test-cluster",
|
||||
vpcStackName: "test-vpc", // Auto-imports VPC ID, CIDR, subnets, numberOfAzs
|
||||
ownerTag: "Test",
|
||||
productTag: "test",
|
||||
componentTag: "cluster",
|
||||
environment: "test",
|
||||
|
||||
// Option A: EC2 with Spot (cheaper than on-demand)
|
||||
instanceType: "t3.small", // Minimal viable instance
|
||||
minClusterSize: 1, // Single instance
|
||||
maxClusterSize: 1, // No scaling
|
||||
spotEnabled: true, // Use Spot for 60-70% savings
|
||||
onDemandPercentage: 0, // 100% Spot
|
||||
enableFargate: true, // Enable Fargate capacity providers
|
||||
|
||||
// Option B: Fargate-only (no EC2 costs)
|
||||
// enableFargate: true,
|
||||
// minClusterSize: 0, // No EC2 instances
|
||||
// maxClusterSize: 0, // No EC2 instances
|
||||
|
||||
containerInsights: false, // Disable Container Insights (saves CloudWatch costs)
|
||||
createExternalLoadBalancer: false, // Service creates its own ALB
|
||||
createInternalLoadBalancer: false,
|
||||
)
|
||||
|
||||
// 3. Service with Blue/Green (pipeline auto-creates ALB stack)
|
||||
spicyECSService(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "test-service", // Pipeline adds -blue/-green suffixes
|
||||
serviceName: "test-api",
|
||||
clusterName: "test-cluster", // Auto-imports VPC from cluster stack
|
||||
vpcStackName: "test-vpc", // Auto-imports subnets, numberOfAzs from VPC stack
|
||||
|
||||
// Container (minimal resources)
|
||||
image: "nexus.kodeniks.com/docker-hosted/test-api:latest",
|
||||
containerPort: 3000,
|
||||
desiredCount: 1, // Single task
|
||||
cpu: 256, // Minimal CPU (0.25 vCPU)
|
||||
memory: 512, // Minimal memory (512 MiB)
|
||||
|
||||
// Use Fargate Spot if cluster has no EC2 instances (Option B above)
|
||||
// capacityProviderStrategy: [
|
||||
// [capacityProvider: "FARGATE_SPOT", weight: 1],
|
||||
// ],
|
||||
|
||||
// Blue/Green Configuration
|
||||
blueGreen: true,
|
||||
hostName: "api-test.example.com", // Auto-generates active/inactive hostnames
|
||||
// bgHostedZoneId: "Z1234567890ABCDEF", // Route53 hosted zone (optional, for Route53 DNS)
|
||||
|
||||
// ALB Configuration (pipeline auto-creates ALB stack if not exists)
|
||||
useClusterAlb: false, // Use individual ALB (not cluster ALB)
|
||||
albScheme: "internet-facing",
|
||||
certificateArn: "arn:aws:acm:ca-central-1:123456789:certificate/xxx",
|
||||
redirectHttpToHttps: true,
|
||||
healthCheckPath: "/health",
|
||||
|
||||
// Tags
|
||||
ownerTag: "Test",
|
||||
productTag: "test",
|
||||
componentTag: "api",
|
||||
environment: "test",
|
||||
|
||||
// Test hooks
|
||||
blueGreenTest: { args, buildInfo ->
|
||||
sh "curl -f https://${buildInfo.inactiveHostname}/health"
|
||||
},
|
||||
smokeTest: { args, buildInfo ->
|
||||
sh "curl -f https://${buildInfo.activeHostname}/health"
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
|
||||
1. **VPC Stack**: Creates 2 AZs with 2 NAT Gateways (~$66/mo)
|
||||
2. **Cluster Stack**: Creates 1 Spot instance (~$5-8/mo) OR 0 instances with Fargate (~$0/mo)
|
||||
3. **ALB Stack**: Pipeline automatically creates `test-service-alb` stack (~$16/mo)
|
||||
4. **Service Stack**: Pipeline creates `test-service-green` (inactive) referencing the ALB
|
||||
5. **DNS**: Route53 records created automatically if `bgHostedZoneId` provided, OR manual Cloudflare setup (see below)
|
||||
6. **Swap**: Pipeline swaps ALB listener rules (no DNS changes)
|
||||
|
||||
**Cost Breakdown:**
|
||||
|
||||
| Component | Cost (EC2) | Cost (Fargate) |
|
||||
| ------------------------- | -------------- | -------------- |
|
||||
| VPC (2 AZs, 2 NAT) | $66/mo | $66/mo |
|
||||
| Cluster (1 Spot instance) | $5-8/mo | $0/mo |
|
||||
| Individual ALB | $16/mo | $16/mo |
|
||||
| Service (1 task) | Minimal | $2-3/mo |
|
||||
| **Total** | **~$87-90/mo** | **~$82-84/mo** |
|
||||
|
||||
**Key Simplifications:**
|
||||
|
||||
- ✅ Uses `vpcStackName` and `clusterName` for auto-imports (no manual subnet IDs)
|
||||
- ✅ Pipeline automatically creates ALB stack (`test-service-alb`)
|
||||
- ✅ Uses `hostName` parameter (auto-generates active/inactive hostnames)
|
||||
- ✅ Minimal resources: 1 Spot instance (or Fargate), 1 task, 256 CPU / 512 MB memory
|
||||
- ✅ Route53 DNS managed automatically (if `bgHostedZoneId` provided)
|
||||
|
||||
## Cloudflare DNS Setup
|
||||
|
||||
If your DNS is hosted with Cloudflare (not Route53), you'll need to manually configure DNS records after deployment:
|
||||
|
||||
1. **Get ALB DNS name** from ALB stack outputs (ALB stack name is `{serviceStackName}-alb`):
|
||||
|
||||
```bash
|
||||
# Get ALB DNS name from ALB stack
|
||||
aws cloudformation describe-stacks \
|
||||
--stack-name test-service-alb \
|
||||
--query 'Stacks[0].Outputs[?OutputKey==`LoadBalancerDNS`].OutputValue' \
|
||||
--output text
|
||||
```
|
||||
|
||||
Or find it in the AWS Console: EC2 → Load Balancers → Your ALB → DNS name
|
||||
|
||||
2. **Create Cloudflare DNS records** (set once, never change):
|
||||
|
||||
**Active hostname:**
|
||||
|
||||
- **Type**: CNAME
|
||||
- **Name**: `api-test` (or your subdomain)
|
||||
- **Target**: `{alb-dns-name}` (e.g., `test-service-alb-123456789.ca-central-1.elb.amazonaws.com`)
|
||||
- **Proxy**: Enabled (orange cloud) for Cloudflare protection
|
||||
- **TTL**: Auto
|
||||
|
||||
**Inactive hostname:**
|
||||
|
||||
- **Name**: `inactive-api-test`
|
||||
- **Target**: Same ALB DNS name (both point to same ALB, routing is by hostname)
|
||||
|
||||
**Note:** These DNS records point to the ALB and never change. The ALB handles routing internally based on the Host header.
|
||||
|
||||
3. **SSL/TLS Settings in Cloudflare**:
|
||||
- Set to **Full** or **Full (strict)** mode
|
||||
- This ensures HTTPS works end-to-end (Cloudflare → ALB → Service)
|
||||
|
||||
### How Blue/Green Works
|
||||
|
||||
```text
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ Cloudflare DNS │
|
||||
│ ┌──────────────────┐ ┌───────────────────────────┐ │
|
||||
│ │ api-test.example │ │ inactive-api-test.example │ │
|
||||
│ │ .com │ │ .com │ │
|
||||
│ └────────┬─────────┘ └──────────────┬────────────┘ │
|
||||
└───────────┼───────────────────────────┼──────────────────┘
|
||||
│ │
|
||||
│ (Both point to same ALB) │
|
||||
└──────────────┬────────────┘
|
||||
▼
|
||||
┌──────────────────────────────┐
|
||||
│ AWS ALB (Individual) │
|
||||
│ ┌─────────────┐ ┌─────────┐ │
|
||||
│ │ Host: api- │ │ Host: │ │
|
||||
│ │ test... │ │ inactive│ │
|
||||
│ │ → BLUE │ │ → GREEN │ │
|
||||
│ └─────┬───────┘ └────┬────┘ │
|
||||
└────────┼──────────────┼──────┘
|
||||
│ │
|
||||
┌────────▼───────┐ ┌────▼──────────┐
|
||||
│ BLUE Service │ │ GREEN Service │
|
||||
│ (Active) │ │ (Inactive) │
|
||||
└────────────────┘ └───────────────┘
|
||||
```
|
||||
|
||||
**Deployment Flow:**
|
||||
|
||||
1. Deploy new version to inactive (GREEN) service
|
||||
2. Test via `inactive-api-test.example.com` in Cloudflare
|
||||
3. Pipeline swaps ALB hostname routing (BLUE ↔ GREEN)
|
||||
4. Active traffic now goes to new version
|
||||
5. Old version stays running for rollback window
|
||||
|
||||
**Important:** The hostname does NOT change during deployments. DNS records point to the ALB (set once, never change), and the ALB routes traffic based on the Host header. The pipeline swaps ALB listener rules (not DNS), so rollback is instant (~30 seconds).
|
||||
|
||||
## Rollback
|
||||
|
||||
The pipeline handles ALB routing swaps automatically. For manual rollback:
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
spicyRollback(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "test-service",
|
||||
serviceName: "test-api",
|
||||
// ... same config as deploy ...
|
||||
activeHostname: "api-test.example.com",
|
||||
inactiveHostname: "inactive-api-test.example.com",
|
||||
// If using Route53:
|
||||
// bgHostedZoneId: "Z1234567890ABCDEF",
|
||||
)
|
||||
```
|
||||
|
||||
This swaps the hostname routing in the ALB (not DNS records), so rollback is instant (~30 seconds). Your DNS records (Route53 or Cloudflare) never change.
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **ACM Certificate**: You'll need an ACM certificate in the same region as your ALB
|
||||
|
||||
- Request via AWS Console or CLI
|
||||
- Must validate domain ownership
|
||||
- Can use DNS validation (add CNAME to Cloudflare)
|
||||
|
||||
2. **Cloudflare Proxy**: Keep proxy enabled (orange cloud) for:
|
||||
|
||||
- DDoS protection
|
||||
- CDN caching (if desired)
|
||||
- SSL termination at Cloudflare edge
|
||||
|
||||
3. **Hostname Routing**: The ALB routes by `Host` header, so both Cloudflare DNS records point to the same ALB DNS name
|
||||
|
||||
4. **Testing**: Use the inactive hostname for integration tests before swapping
|
||||
|
||||
5. **Fargate vs EC2**: For cheapest option, use Fargate-only (set `minClusterSize: 0` and `maxClusterSize: 0` in cluster, then use `FARGATE_SPOT` capacity provider in service)
|
||||
|
||||
## Cleanup
|
||||
|
||||
Remember to destroy test stacks when done:
|
||||
|
||||
```bash
|
||||
# Via Jenkins or AWS CLI
|
||||
aws cloudformation delete-stack --stack-name test-service-blue
|
||||
aws cloudformation delete-stack --stack-name test-service-green
|
||||
aws cloudformation delete-stack --stack-name test-service-alb
|
||||
aws cloudformation delete-stack --stack-name test-cluster
|
||||
aws cloudformation delete-stack --stack-name test-vpc
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
**Barebones blue/green setup:**
|
||||
|
||||
- VPC: 2 AZs, 2 NAT Gateways
|
||||
- Cluster: 1 Spot instance (or Fargate-only with 0 instances)
|
||||
- Service: 1 task with blue/green deployment
|
||||
- ALB: Individual internet-facing ALB (auto-created by pipeline)
|
||||
- DNS: Route53 (automatic) or Cloudflare (manual)
|
||||
- **Total: ~$87-90/month (EC2) or ~$82-84/month (Fargate)**
|
||||
365
docs/DEVELOPMENT.md
Normal file
365
docs/DEVELOPMENT.md
Normal file
@@ -0,0 +1,365 @@
|
||||
# Local Development Guide
|
||||
|
||||
Run and test CDK locally without Jenkins.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- **Node.js 22+**
|
||||
- **pnpm** (package manager)
|
||||
- **AWS CLI** (configured with credentials)
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
cd spicy-automation
|
||||
|
||||
# Install dependencies
|
||||
pnpm install
|
||||
```
|
||||
|
||||
## Bootstrapping
|
||||
|
||||
Before deploying any CDK stacks, you must bootstrap the CDK environment in your AWS account and region. This is a **one-time setup** per account/region combination.
|
||||
|
||||
### Bootstrap Command
|
||||
|
||||
```bash
|
||||
# Using Docker (same as your deploy command)
|
||||
docker run -v ~/.aws:/root/.aws:ro spicy-sdk \
|
||||
cdk bootstrap \
|
||||
aws://ACCOUNT_ID/REGION
|
||||
|
||||
# Or locally with npx
|
||||
npx cdk bootstrap aws://ACCOUNT_ID/REGION
|
||||
|
||||
# Example for ca-central-1
|
||||
npx cdk bootstrap aws://$AWS_ACCOUNT_ID/ca-central-1
|
||||
```
|
||||
|
||||
### What Bootstrap Does
|
||||
|
||||
CDK bootstrap creates the following resources in your AWS account:
|
||||
|
||||
- S3 bucket for storing CDK assets
|
||||
- IAM roles for deployment
|
||||
- SSM parameters for tracking bootstrap version
|
||||
|
||||
### Verify Bootstrap Status
|
||||
|
||||
```bash
|
||||
# Check if environment is bootstrapped
|
||||
aws ssm get-parameter \
|
||||
--name /cdk-bootstrap/hnb659fds/version \
|
||||
--region ca-central-1
|
||||
```
|
||||
|
||||
If the parameter exists, the environment is bootstrapped. If you get a `ParameterNotFound` error, you need to bootstrap.
|
||||
|
||||
### Bootstrap Qualifier
|
||||
|
||||
This project uses the bootstrap qualifier `hnb659fds` (configured in `cdk.json`). This qualifier must match between bootstrap and deploy commands.
|
||||
|
||||
## Running CDK Commands
|
||||
|
||||
CDK runs directly with `ts-node` - no build step required!
|
||||
|
||||
### Synthesize a Template
|
||||
|
||||
```bash
|
||||
# Minimal VPC
|
||||
npx cdk synth \
|
||||
-c stackType=vpc \
|
||||
-c stackName=my-vpc \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myapp \
|
||||
-c componentTag=vpc
|
||||
|
||||
# With custom options
|
||||
npx cdk synth \
|
||||
-c stackType=vpc \
|
||||
-c stackName=my-vpc \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myapp \
|
||||
-c componentTag=vpc \
|
||||
-c vpcCidr=10.0.0.0/16 \
|
||||
-c numberOfAzs=2 \
|
||||
-c createAdditionalPrivateSubnets=false
|
||||
```
|
||||
|
||||
### Show Diff
|
||||
|
||||
```bash
|
||||
npx cdk diff \
|
||||
-c stackType=vpc \
|
||||
-c stackName=my-vpc \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myapp \
|
||||
-c componentTag=vpc
|
||||
```
|
||||
|
||||
### Deploy
|
||||
|
||||
```bash
|
||||
# Requires AWS credentials
|
||||
export AWS_PROFILE=my-profile
|
||||
# or
|
||||
export AWS_ACCESS_KEY_ID=...
|
||||
export AWS_SECRET_ACCESS_KEY=...
|
||||
export AWS_REGION=ca-central-1
|
||||
|
||||
npx cdk deploy \
|
||||
-c stackType=vpc \
|
||||
-c stackName=my-vpc \
|
||||
-c ownerTag=MyTeam \
|
||||
-c productTag=myapp \
|
||||
-c componentTag=vpc
|
||||
```
|
||||
|
||||
### Destroy
|
||||
|
||||
```bash
|
||||
npx cdk destroy \
|
||||
-c stackType=vpc \
|
||||
-c stackName=my-vpc \
|
||||
-c stackType=vpc
|
||||
```
|
||||
|
||||
## Context Parameters
|
||||
|
||||
All VPC parameters can be passed via `-c key=value`:
|
||||
|
||||
| Context Key | Type | Description |
|
||||
| -------------------------------- | ------ | ----------------------------------------------- |
|
||||
| `stackType` | string | Stack type: `vpc`, `ecs-cluster`, `ecs-service` |
|
||||
| `stackName` | string | CloudFormation stack name |
|
||||
| `ownerTag` | string | Owner tag |
|
||||
| `productTag` | string | Product tag |
|
||||
| `componentTag` | string | Component tag |
|
||||
| `build` | string | Build tag (defaults to git SHA) |
|
||||
| `vpcCidr` | string | VPC CIDR block |
|
||||
| `vpcTenancy` | string | `default` or `dedicated` |
|
||||
| `numberOfAzs` | string | `2`, `3`, or `4` |
|
||||
| `availabilityZones` | string | Comma-separated AZ list |
|
||||
| `createPrivateSubnets` | string | `true` or `false` |
|
||||
| `createAdditionalPrivateSubnets` | string | `true` or `false` |
|
||||
| `publicSubnetTag` | string | Tag for public subnets |
|
||||
| `privateSubnetATag` | string | Tag for private subnets type 1 |
|
||||
| `privateSubnetBTag` | string | Tag for private subnets type 2 |
|
||||
|
||||
## Running Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
pnpm test
|
||||
|
||||
# Run with coverage
|
||||
pnpm test -- --coverage
|
||||
|
||||
# Run specific test
|
||||
pnpm test -- --testNamePattern="creates VPC"
|
||||
|
||||
# Watch mode
|
||||
pnpm test -- --watch
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
spicy-automation/
|
||||
├── bin/
|
||||
│ └── spicy-cdk.ts # CDK app entry point
|
||||
├── lib/
|
||||
│ ├── constructs/ # Reusable constructs
|
||||
│ │ ├── spicy-vpc.ts # VPC construct
|
||||
│ │ └── index.ts
|
||||
│ ├── stacks/ # Stack definitions
|
||||
│ │ ├── spicy-vpc-stack.ts
|
||||
│ │ └── index.ts
|
||||
│ └── index.ts
|
||||
├── test/
|
||||
│ └── spicy-cdk.test.ts # Jest tests
|
||||
├── jenkins/
|
||||
│ └── vars/ # Jenkins shared library
|
||||
├── cdk.json # CDK configuration
|
||||
├── tsconfig.json # TypeScript configuration
|
||||
└── package.json
|
||||
```
|
||||
|
||||
## Adding a New Construct
|
||||
|
||||
### 1. Create the Construct
|
||||
|
||||
`lib/constructs/my-construct.ts`:
|
||||
|
||||
```typescript
|
||||
import * as cdk from 'aws-cdk-lib/core';
|
||||
import { Construct } from 'constructs';
|
||||
|
||||
export interface MyConstructProps {
|
||||
readonly name: string;
|
||||
// ... other props
|
||||
}
|
||||
|
||||
export class MyConstruct extends Construct {
|
||||
constructor(scope: Construct, id: string, props: MyConstructProps) {
|
||||
super(scope, id);
|
||||
|
||||
// Create resources...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Export from Index
|
||||
|
||||
`lib/constructs/index.ts`:
|
||||
|
||||
```typescript
|
||||
export * from './spicy-vpc';
|
||||
export * from './my-construct';
|
||||
```
|
||||
|
||||
### 3. Create a Stack
|
||||
|
||||
`lib/stacks/my-stack.ts`:
|
||||
|
||||
```typescript
|
||||
import * as cdk from 'aws-cdk-lib/core';
|
||||
import { Construct } from 'constructs';
|
||||
import { MyConstruct, MyConstructProps } from '../constructs/my-construct';
|
||||
|
||||
export class MyStack extends cdk.Stack {
|
||||
constructor(scope: Construct, id: string, props: MyConstructProps & cdk.StackProps) {
|
||||
super(scope, id, props);
|
||||
|
||||
new MyConstruct(this, 'MyConstruct', props);
|
||||
}
|
||||
|
||||
static fromContext(scope: Construct, id: string, stackProps?: cdk.StackProps): MyStack {
|
||||
const app = scope.node.root as cdk.App;
|
||||
|
||||
return new MyStack(app, id, {
|
||||
...stackProps,
|
||||
name: app.node.tryGetContext('name') ?? 'default',
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Register in App
|
||||
|
||||
`bin/spicy-cdk.ts`:
|
||||
|
||||
```typescript
|
||||
import { MyStack } from '../lib/stacks';
|
||||
|
||||
switch (stackType) {
|
||||
case 'vpc':
|
||||
SpicyVpcStack.fromContext(app, stackName, { env });
|
||||
break;
|
||||
case 'my-stack':
|
||||
MyStack.fromContext(app, stackName, { env });
|
||||
break;
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Add Tests
|
||||
|
||||
`test/my-stack.test.ts`:
|
||||
|
||||
```typescript
|
||||
import { Template } from 'aws-cdk-lib/assertions';
|
||||
import * as cdk from 'aws-cdk-lib/core';
|
||||
import { MyStack } from '../lib/stacks/my-stack';
|
||||
|
||||
describe('MyStack', () => {
|
||||
test('creates resources', () => {
|
||||
const app = new cdk.App();
|
||||
const stack = new MyStack(app, 'TestStack', {
|
||||
name: 'test',
|
||||
});
|
||||
|
||||
const template = Template.fromStack(stack);
|
||||
|
||||
// Assert on resources
|
||||
template.resourceCountIs('AWS::SomeService::Resource', 1);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## TypeScript Configuration
|
||||
|
||||
The project uses CommonJS modules for compatibility with ts-node:
|
||||
|
||||
```json
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "CommonJS",
|
||||
"strict": true,
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Debugging
|
||||
|
||||
### Enable CDK Debug Output
|
||||
|
||||
```bash
|
||||
CDK_DEBUG=true npx cdk synth ...
|
||||
```
|
||||
|
||||
### View Synthesized Template
|
||||
|
||||
Templates are output to `cdk.out/`:
|
||||
|
||||
```bash
|
||||
npx cdk synth -c stackType=vpc -c stackName=test ...
|
||||
cat cdk.out/test.template.json | jq .
|
||||
```
|
||||
|
||||
### Check Logical IDs
|
||||
|
||||
Verify resource logical IDs are stable:
|
||||
|
||||
```bash
|
||||
npx cdk synth ... 2>&1 | grep -E "^ [A-Za-z]+:"
|
||||
```
|
||||
|
||||
## Building (Optional)
|
||||
|
||||
While not required (ts-node runs TypeScript directly), you can compile:
|
||||
|
||||
```bash
|
||||
# Compile to JavaScript
|
||||
pnpm run build
|
||||
|
||||
# Watch mode
|
||||
pnpm run watch
|
||||
```
|
||||
|
||||
Output goes to `dist/`.
|
||||
|
||||
## IDE Setup
|
||||
|
||||
### VS Code
|
||||
|
||||
Recommended extensions:
|
||||
|
||||
- ESLint
|
||||
- Prettier
|
||||
- AWS Toolkit
|
||||
|
||||
`.vscode/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"typescript.tsdk": "node_modules/typescript/lib",
|
||||
"editor.formatOnSave": true
|
||||
}
|
||||
```
|
||||
|
||||
### Cursor
|
||||
|
||||
The project works out of the box with Cursor's TypeScript support.
|
||||
384
docs/ECS_CLUSTER.md
Normal file
384
docs/ECS_CLUSTER.md
Normal file
@@ -0,0 +1,384 @@
|
||||
# ECS Cluster Deployment
|
||||
|
||||
Deploy production-ready ECS clusters with AWS CDK.
|
||||
|
||||
## Features
|
||||
|
||||
- **EC2 Capacity Provider** with managed scaling (replaces custom SchedulableContainers metric)
|
||||
- **Mixed Instances Policy** for Spot support (replaces Autospotting)
|
||||
- **Launch Templates** with IMDSv2 and gp3 EBS volumes
|
||||
- **Instance Draining** via lifecycle hooks for graceful task migration
|
||||
- **Optional Fargate** capacity providers for serverless workloads
|
||||
- **Internal/External ALBs** with HTTPS support
|
||||
- **Container Insights** for monitoring
|
||||
- **Automatic instance refresh** via max instance lifetime
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Minimal Jenkinsfile - Using CloudFormation Imports
|
||||
|
||||
**Minimal props:** Only `vpcStackName` required. All VPC details auto-import from VPC stack exports.
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
spicyECSCluster(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "my-ecs-cluster",
|
||||
vpcStackName: "my-vpc", // Auto-imports ALL VPC details (VPC ID, CIDR, subnets, AZs)
|
||||
ownerTag: "MyTeam",
|
||||
productTag: "my-product",
|
||||
componentTag: "ecs-cluster",
|
||||
environment: "dev"
|
||||
)
|
||||
```
|
||||
|
||||
**What auto-imports from VPC stack:**
|
||||
|
||||
- VPC ID from `${vpcStackName}-VPCID`
|
||||
- VPC CIDR from `${vpcStackName}-VPCCIDR`
|
||||
- Number of AZs from `${vpcStackName}-NumberOfAZs`
|
||||
- Private subnet IDs from `${vpcStackName}-PrivateSubnetA1ID`, `${vpcStackName}-PrivateSubnetB1ID`, etc.
|
||||
- Public subnet IDs from `${vpcStackName}-PublicSubnetAID`, `${vpcStackName}-PublicSubnetBID`, etc. (if `createExternalLoadBalancer: true`)
|
||||
- Availability zones auto-derived from region and number of AZs
|
||||
|
||||
### Production Jenkinsfile with All Options
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
spicyECSCluster(
|
||||
// AWS Configuration
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
accountId: "123456789012",
|
||||
stackName: "prod-ecs-cluster",
|
||||
|
||||
// VPC Configuration - only vpcStackName required, all VPC details auto-import
|
||||
vpcStackName: "production-vpc",
|
||||
// VPC ID, CIDR, subnets, AZs, and numberOfAzs all auto-import from VPC stack exports
|
||||
|
||||
// Tags
|
||||
ownerTag: "Platform",
|
||||
productTag: "spicy",
|
||||
componentTag: "ecs-cluster",
|
||||
environment: "prod",
|
||||
|
||||
// Instance Configuration
|
||||
instanceType: "m5a.xlarge",
|
||||
additionalInstanceTypes: "m5.xlarge,m5d.xlarge,m5n.xlarge",
|
||||
keyName: "my-keypair",
|
||||
ebsVolumeSize: 100,
|
||||
|
||||
// Scaling
|
||||
minClusterSize: 3,
|
||||
maxClusterSize: 10,
|
||||
targetCapacityPercent: 100,
|
||||
|
||||
// Spot Configuration (for cost savings)
|
||||
spotEnabled: true,
|
||||
onDemandPercentage: 50, // 50% On-Demand, 50% Spot
|
||||
spotAllocationStrategy: "capacity-optimized",
|
||||
|
||||
// Load Balancers
|
||||
createExternalLoadBalancer: true,
|
||||
createInternalLoadBalancer: true,
|
||||
certificateArn: "arn:aws:acm:ca-central-1:123456789012:certificate/xxx",
|
||||
|
||||
// Fargate (optional hybrid - enables both FARGATE and FARGATE_SPOT)
|
||||
enableFargate: false,
|
||||
|
||||
// Timeouts
|
||||
drainingTimeout: 900, // 15 minutes for task draining
|
||||
maxInstanceLifetime: 604800, // 7 days for instance refresh
|
||||
|
||||
// Container Insights
|
||||
containerInsights: true,
|
||||
|
||||
// Approval for production
|
||||
approvers: "admin,platform-team"
|
||||
)
|
||||
```
|
||||
|
||||
## Parameters Reference
|
||||
|
||||
### Required Parameters
|
||||
|
||||
| Parameter | Description | Example |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------- | ------------------- |
|
||||
| `jenkinsAwsCredentialsId` | Jenkins credential ID for AWS | `"aws-credentials"` |
|
||||
| `region` | AWS region | `"ca-central-1"` |
|
||||
| `stackName` | CloudFormation stack name | `"my-ecs-cluster"` |
|
||||
| `vpcStackName` | VPC stack name - **required**. All VPC details (VPC ID, CIDR, subnets, AZs) auto-import from VPC stack exports | `"my-vpc"` |
|
||||
| `ownerTag` | Owner tag value | `"MyTeam"` |
|
||||
| `productTag` | Product tag value | `"my-product"` |
|
||||
|
||||
### Instance Configuration
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------------------- | ----------- | ----------------------------------- |
|
||||
| `instanceType` | `m5a.large` | Primary EC2 instance type |
|
||||
| `additionalInstanceTypes` | - | Additional types for Spot diversity |
|
||||
| `keyName` | - | EC2 key pair for SSH access |
|
||||
| `ebsVolumeSize` | `100` | EBS volume size in GB |
|
||||
| `containerInsights` | `true` | Enable Container Insights |
|
||||
|
||||
### Scaling Configuration
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ----------------------- | ------- | -------------------------------------- |
|
||||
| `minClusterSize` | `2` | Minimum number of instances |
|
||||
| `maxClusterSize` | `4` | Maximum number of instances |
|
||||
| `targetCapacityPercent` | `100` | Target utilization for managed scaling |
|
||||
|
||||
### Spot Configuration
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------------------ | -------------------- | -------------------------------------- |
|
||||
| `spotEnabled` | `false` | Enable Spot instances |
|
||||
| `onDemandPercentage` | `100` | Percentage of On-Demand (rest is Spot) |
|
||||
| `spotAllocationStrategy` | `capacity-optimized` | Spot allocation strategy |
|
||||
|
||||
**Spot Allocation Strategies:**
|
||||
|
||||
- `capacity-optimized` - Best for interruption avoidance (recommended)
|
||||
- `lowest-price` - Best for cost, higher interruption risk
|
||||
- `capacity-optimized-prioritized` - Prioritizes instance types you specify
|
||||
|
||||
### Load Balancer Configuration
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ---------------------------- | ------- | ----------------------------------------------------------------------------------- |
|
||||
| `createExternalLoadBalancer` | `false` | Create internet-facing ALB (public subnets auto-imported from VPC stack if enabled) |
|
||||
| `createInternalLoadBalancer` | `false` | Create internal ALB |
|
||||
| `certificateArn` | - | ACM certificate for HTTPS |
|
||||
|
||||
### Fargate Configuration
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| --------------- | ------- | ---------------------------------------------------------------------- |
|
||||
| `enableFargate` | `false` | Enable Fargate capacity providers (adds both FARGATE and FARGATE_SPOT) |
|
||||
|
||||
### Lifecycle Configuration
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| --------------------- | -------- | --------------------------------- |
|
||||
| `drainingTimeout` | `900` | Seconds to wait for task draining |
|
||||
| `maxInstanceLifetime` | `604800` | Max instance age (7 days) |
|
||||
|
||||
## Environment-Specific Configuration
|
||||
|
||||
### Development/Sandbox
|
||||
|
||||
```groovy
|
||||
spicyECSCluster(
|
||||
// ... base config ...
|
||||
environment: "dev",
|
||||
minClusterSize: 1,
|
||||
maxClusterSize: 2,
|
||||
spotEnabled: true,
|
||||
onDemandPercentage: 0, // 100% Spot for max savings
|
||||
)
|
||||
```
|
||||
|
||||
### Staging
|
||||
|
||||
```groovy
|
||||
spicyECSCluster(
|
||||
// ... base config ...
|
||||
environment: "staging",
|
||||
minClusterSize: 2,
|
||||
maxClusterSize: 4,
|
||||
spotEnabled: true,
|
||||
onDemandPercentage: 20, // 80% Spot
|
||||
)
|
||||
```
|
||||
|
||||
### Production
|
||||
|
||||
```groovy
|
||||
spicyECSCluster(
|
||||
// ... base config ...
|
||||
environment: "prod",
|
||||
minClusterSize: 3,
|
||||
maxClusterSize: 10,
|
||||
spotEnabled: true,
|
||||
onDemandPercentage: 50, // 50% On-Demand baseline
|
||||
approvers: "admin,platform-team"
|
||||
)
|
||||
```
|
||||
|
||||
## Stack Outputs
|
||||
|
||||
The stack exports these values for use by ECS services:
|
||||
|
||||
| Output | Export Name | Description |
|
||||
| -------------------------- | -------------------------------------------- | ---------------------- |
|
||||
| `ClusterName` | `{stackName}-cluster-name` | ECS cluster name |
|
||||
| `ClusterArn` | `{stackName}-cluster-arn` | ECS cluster ARN |
|
||||
| `VPC` | `{stackName}-VPC` | VPC ID |
|
||||
| `ECSHostSecurityGroup` | `{stackName}-ecs-host-security-group` | EC2 security group |
|
||||
| `AutoScalingGroupName` | `{stackName}-auto-scaling-group` | ASG name |
|
||||
| `ExternalLoadBalancerDNS` | `{stackName}-internet-facing-url` | External ALB DNS |
|
||||
| `ExternalLoadBalancerArn` | `{stackName}-internet-facing-arn` | External ALB ARN |
|
||||
| `ExternalHTTPListenerArn` | `{stackName}-internet-facing-http-listener` | HTTP listener ARN |
|
||||
| `ExternalHTTPSListenerArn` | `{stackName}-internet-facing-https-listener` | HTTPS listener ARN |
|
||||
| `InternalLoadBalancerDNS` | `{stackName}-internal-url` | Internal ALB DNS |
|
||||
| `InternalLoadBalancerArn` | `{stackName}-internal-arn` | Internal ALB ARN |
|
||||
| `InternalHTTPListenerArn` | `{stackName}-internal-http-listener` | HTTP listener ARN |
|
||||
| `InternalHTTPSListenerArn` | `{stackName}-internal-https-listener` | HTTPS listener ARN |
|
||||
| `LogsBucketName` | `{stackName}-logs-s3-bucket` | ALB access logs bucket |
|
||||
|
||||
## How It Works
|
||||
|
||||
### Capacity Providers (Replaces Custom Scaling)
|
||||
|
||||
The cluster uses **ECS Managed Scaling** via Capacity Providers:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ ECS Cluster │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Capacity Providers: │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ EC2 Capacity Provider │ │
|
||||
│ │ - Managed Scaling: ON │ │
|
||||
│ │ - Target Capacity: 100% │ │
|
||||
│ │ - Min Scaling Step: 1 │ │
|
||||
│ │ - Max Scaling Step: 10000 │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ FARGATE (optional) │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ FARGATE_SPOT (optional) │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
This replaces the legacy `SchedulableContainers` Lambda metric with AWS-native scaling.
|
||||
|
||||
### Mixed Instances Policy (Replaces Autospotting)
|
||||
|
||||
When `spotEnabled: true`:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Auto Scaling Group │
|
||||
├─────────────────────────────────────────────────────────┤
|
||||
│ Mixed Instances Policy: │
|
||||
│ ┌─────────────────────────────────────────────────┐ │
|
||||
│ │ On-Demand Base Capacity: 0 │ │
|
||||
│ │ On-Demand % Above Base: 50% │ │
|
||||
│ │ Spot Allocation: capacity-optimized │ │
|
||||
│ └─────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
│ Instance Type Overrides: │
|
||||
│ - m5a.large (primary) │
|
||||
│ - m5.large │
|
||||
│ - m5d.large │
|
||||
│ - m5n.large │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
Benefits over Autospotting:
|
||||
|
||||
- No Lambda to maintain
|
||||
- Faster response (no polling delay)
|
||||
- Better capacity data (AWS-native)
|
||||
- Simpler architecture
|
||||
|
||||
### Instance Draining
|
||||
|
||||
Two-layer draining for zero-downtime:
|
||||
|
||||
1. **Spot Interruption Draining** (native ECS):
|
||||
|
||||
```bash
|
||||
ECS_ENABLE_SPOT_INSTANCE_DRAINING=true
|
||||
```
|
||||
|
||||
ECS agent drains tasks on 2-minute Spot termination notice.
|
||||
|
||||
2. **Lifecycle Hook Draining** (Lambda):
|
||||
- ASG sends termination event to SNS
|
||||
- Lambda sets instance to DRAINING
|
||||
- Waits for running tasks to migrate
|
||||
- Completes lifecycle action
|
||||
|
||||
### Launch Template Features
|
||||
|
||||
- **IMDSv2 Required**: Enhanced metadata security
|
||||
- **gp3 EBS Volumes**: Better performance, lower cost than gp2
|
||||
- **Encrypted Volumes**: EBS encryption enabled
|
||||
- **SSM Agent**: Pre-installed for Session Manager access
|
||||
|
||||
## Migrating from Legacy Automation
|
||||
|
||||
### Parameter Mapping
|
||||
|
||||
| Legacy (Ansible) | New (CDK) |
|
||||
| ----------------------------------- | ------------------------------ |
|
||||
| `stackName` | `stackName` |
|
||||
| `instanceType` | `instanceType` |
|
||||
| `minClusterSize` | `minClusterSize` |
|
||||
| `maxClusterSize` | `maxClusterSize` |
|
||||
| `spotEnabled` | `spotEnabled` |
|
||||
| `minOnDemandPercentage` | `onDemandPercentage` |
|
||||
| `largestContainerCPUReservation` | (not needed - managed scaling) |
|
||||
| `largestContainerMemoryReservation` | (not needed - managed scaling) |
|
||||
| `clusterScaleUpAdjustment` | (not needed - managed scaling) |
|
||||
| `clusterScaleDownAdjustment` | (not needed - managed scaling) |
|
||||
|
||||
### Removed Features
|
||||
|
||||
These legacy features are no longer needed:
|
||||
|
||||
- **SchedulableContainers Lambda**: Replaced by Capacity Provider managed scaling
|
||||
- **Autospotting**: Replaced by Mixed Instances Policy
|
||||
- **Launch Configurations**: Replaced by Launch Templates
|
||||
- **gp2 volumes**: Upgraded to gp3
|
||||
- **IMDSv1**: Now requires IMDSv2
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Instances Not Joining Cluster
|
||||
|
||||
Check the ECS agent logs:
|
||||
|
||||
```bash
|
||||
docker logs ecs-agent
|
||||
cat /var/log/ecs/ecs-agent.log
|
||||
```
|
||||
|
||||
Verify cluster name in user data:
|
||||
|
||||
```bash
|
||||
cat /etc/ecs/ecs.config
|
||||
```
|
||||
|
||||
### Tasks Not Draining
|
||||
|
||||
Check Lambda logs in CloudWatch:
|
||||
|
||||
```
|
||||
/aws/lambda/{stackName}-DrainingLambda
|
||||
```
|
||||
|
||||
### Spot Interruptions
|
||||
|
||||
Monitor with CloudWatch metrics:
|
||||
|
||||
- `AWS/EC2Spot` → `InterruptionRate`
|
||||
- `AWS/ECS` → `CPUReservation`, `MemoryReservation`
|
||||
|
||||
Consider increasing `onDemandPercentage` for critical workloads.
|
||||
|
||||
## Cost Optimization Tips
|
||||
|
||||
1. **Use Spot in non-prod**: `onDemandPercentage: 0`
|
||||
2. **Multiple instance types**: Better Spot availability
|
||||
3. **Right-size instances**: Match to your container sizes
|
||||
4. **Enable Fargate Spot**: For batch/background tasks
|
||||
5. **Set max instance lifetime**: Force instance refresh for patches
|
||||
867
docs/ECS_SERVICE.md
Normal file
867
docs/ECS_SERVICE.md
Normal file
@@ -0,0 +1,867 @@
|
||||
# ECS Service Deployment
|
||||
|
||||
Deploy ECS services with mixed capacity provider strategies for EC2 + Fargate burst.
|
||||
|
||||
## Features
|
||||
|
||||
- **Blue/Green Deployments** - Zero-downtime deploys with instant rollback via hostname swap
|
||||
- **Mixed Capacity Provider Strategy** - EC2 base + Fargate burst for cost optimization
|
||||
- **Auto-scaling** on CPU, Memory, and ALB request count
|
||||
- **ALB Integration** with host/path routing
|
||||
- **Deployment Circuit Breaker** with automatic rollback
|
||||
- **ECS Exec** for interactive debugging
|
||||
- **Secrets Manager** integration
|
||||
- **CloudWatch Logs** with configurable retention
|
||||
- **Pipeline Hooks** - Custom behavior at every stage
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Minimal Jenkinsfile (EC2 Only) - Using CloudFormation Imports
|
||||
|
||||
**Minimal props:** Only `clusterName` required. VPC info auto-imports from cluster stack exports.
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
spicyECSService(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "my-api-dev",
|
||||
serviceName: "my-api",
|
||||
|
||||
// Cluster info - VPC ID auto-imports from ${clusterName}-VPC
|
||||
clusterName: "my-cluster-dev",
|
||||
|
||||
// Container
|
||||
image: "nexus.kodeniks.com/docker-hosted/my-api:latest",
|
||||
containerPort: 3000,
|
||||
|
||||
// Tags
|
||||
ownerTag: "MyTeam",
|
||||
productTag: "my-product",
|
||||
componentTag: "api",
|
||||
environment: "dev"
|
||||
)
|
||||
```
|
||||
|
||||
**What auto-imports:**
|
||||
|
||||
- VPC stack name from `${clusterStackName}-VPCStackName` (cluster stack export)
|
||||
- VPC ID from `${clusterStackName}-VPC` (cluster stack export)
|
||||
- VPC CIDR from `${vpcStackName}-VPCCIDR` (VPC stack export, using imported VPC stack name)
|
||||
- Private subnets from `${vpcStackName}-PrivateSubnetA1ID`, etc. (VPC stack exports)
|
||||
- Number of AZs from `${vpcStackName}-NumberOfAZs` (VPC stack export)
|
||||
- Logs bucket from `${clusterStackName}-logs-s3-bucket` (cluster stack export)
|
||||
|
||||
**Note:** The service stack imports the VPC stack name from the cluster stack export `${clusterStackName}-VPCStackName`, then uses that to import all VPC details (CIDR, subnets, AZs) from the VPC stack exports.
|
||||
|
||||
### Mixed Capacity Strategy (EC2 + Fargate Burst)
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
spicyECSService(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "my-api-prod",
|
||||
serviceName: "my-api",
|
||||
|
||||
// Cluster info - VPC details auto-import from cluster/VPC stack exports
|
||||
clusterName: "my-cluster-prod",
|
||||
vpcStackName: "my-vpc", // Optional: auto-imported from cluster stack if not provided
|
||||
|
||||
// Container
|
||||
image: "nexus.kodeniks.com/docker-hosted/my-api:v1.2.3",
|
||||
containerPort: 3000,
|
||||
cpu: 512,
|
||||
memory: 1024,
|
||||
|
||||
// Mixed capacity strategy
|
||||
capacityProviderStrategy: [
|
||||
[capacityProvider: "my-cluster-prod-ec2", base: 2, weight: 3], // First 2 on EC2, then 75%
|
||||
[capacityProvider: "FARGATE_SPOT", weight: 1], // 25% burst to Fargate Spot
|
||||
],
|
||||
|
||||
// Scaling
|
||||
desiredCount: 2,
|
||||
minCapacity: 2,
|
||||
maxCapacity: 20,
|
||||
targetCpuUtilization: 70,
|
||||
targetMemoryUtilization: 80,
|
||||
|
||||
// Routing - Use cluster ALB (default)
|
||||
useClusterAlb: true, // Use cluster ALB (default)
|
||||
albScheme: "internet-facing", // or "internal"
|
||||
hostHeader: "api.example.com",
|
||||
priority: 100,
|
||||
healthCheckPath: "/health",
|
||||
|
||||
// Tags
|
||||
ownerTag: "Platform",
|
||||
productTag: "my-product",
|
||||
componentTag: "api",
|
||||
environment: "prod",
|
||||
|
||||
// Approval for prod
|
||||
approvers: "admin,platform-team"
|
||||
)
|
||||
```
|
||||
|
||||
## Parameters Reference
|
||||
|
||||
### Required Parameters
|
||||
|
||||
| Parameter | Description | Example |
|
||||
| ------------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
|
||||
| `jenkinsAwsCredentialsId` | Jenkins credential ID for AWS | `"aws-credentials"` |
|
||||
| `region` | AWS region | `"ca-central-1"` |
|
||||
| `stackName` | CloudFormation stack name | `"my-api-dev"` |
|
||||
| `serviceName` | ECS service name | `"my-api"` |
|
||||
| `clusterName` | ECS cluster name (required) - used to auto-import VPC stack name, VPC ID, and other details from cluster stack exports | `"my-cluster-dev"` |
|
||||
| `clusterStackName` | Cluster stack name (defaults to `clusterName`) - used for CloudFormation imports | `"my-cluster-dev"` |
|
||||
| `vpcStackName` | VPC stack name (auto-imported from `${clusterStackName}-VPCStackName`, or provide explicitly) | `"my-vpc"` |
|
||||
| `albStackName` | ALB stack name (for individual ALB) - used to auto-import ALB ARN and listener ARNs from ALB stack | `"my-service-alb"` |
|
||||
| `image` | Docker image URI | `"nexus.kodeniks.com/docker-hosted/my-api:latest"` |
|
||||
| `containerPort` | Container port | `3000` |
|
||||
| `ownerTag` | Owner tag | `"MyTeam"` |
|
||||
| `productTag` | Product tag | `"my-product"` |
|
||||
|
||||
### Container Configuration
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------------ | ------- | --------------------------------------- |
|
||||
| `containerPort` | `3000` | Container port |
|
||||
| `cpu` | `256` | CPU units (256 = 0.25 vCPU) |
|
||||
| `memory` | `512` | Memory in MiB |
|
||||
| `environment_vars` | - | Map of environment variables |
|
||||
| `secrets` | - | Map of secret ARNs (env var name → ARN) |
|
||||
|
||||
### Capacity Provider Strategy
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| -------------------------- | --------------- | ---------------------------------- |
|
||||
| `capacityProviderStrategy` | Cluster default | Array of capacity provider configs |
|
||||
| `desiredCount` | `2` | Initial task count |
|
||||
|
||||
**Strategy Item Format:**
|
||||
|
||||
```groovy
|
||||
[
|
||||
capacityProvider: "provider-name", // EC2 provider name, "FARGATE", or "FARGATE_SPOT"
|
||||
base: 0, // Minimum tasks on this provider
|
||||
weight: 1 // Distribution weight
|
||||
]
|
||||
```
|
||||
|
||||
### Auto-Scaling
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------------------- | ------- | ------------------------------------ |
|
||||
| `minCapacity` | - | Minimum tasks (required for scaling) |
|
||||
| `maxCapacity` | - | Maximum tasks (required for scaling) |
|
||||
| `targetCpuUtilization` | - | Target CPU % (0-100) |
|
||||
| `targetMemoryUtilization` | - | Target memory % (0-100) |
|
||||
| `targetRequestsPerTarget` | - | Target requests per task per minute |
|
||||
|
||||
### ALB Routing
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| --------------------- | ------- | -------------------------------------------------------------------------------------------------- |
|
||||
| `useClusterAlb` | `true` | Use cluster ALB (default). Set to `false` to use individual ALB per service |
|
||||
| `albScheme` | - | ALB scheme: `"internet-facing"` or `"internal"` (required when `useClusterAlb=true`) |
|
||||
| `albStackName` | - | ALB stack name for individual ALB (auto-derived from `{stackName}-alb` when `useClusterAlb=false`) |
|
||||
| `certificateArn` | - | ACM certificate ARN (required for individual ALB when `useClusterAlb=false`) |
|
||||
| `redirectHttpToHttps` | - | Redirect HTTP to HTTPS (for individual ALB) |
|
||||
| `hostHeader` | - | Host header for routing |
|
||||
| `pathPatterns` | - | Comma-separated path patterns |
|
||||
| `priority` | `100` | Listener rule priority |
|
||||
| `stickiness` | `false` | Enable session stickiness |
|
||||
| `stickinessDuration` | `86400` | Stickiness duration (seconds) |
|
||||
|
||||
### Health Check
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ----------------- | --------- | --------------------- |
|
||||
| `healthCheckPath` | `/health` | Health check URL path |
|
||||
|
||||
### DNS (Route53) - Blue/Green Only
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ------------------ | ------- | -------------------------------------------------------------------------- |
|
||||
| `bgHostedZoneId` | - | Route53 hosted zone ID for blue/green DNS records (optional) |
|
||||
| `hostName` | - | Simple hostname (e.g., "api.example.com") - auto-generates active/inactive |
|
||||
| `activeHostname` | - | Active hostname (e.g., "api.example.com") - explicit hostname |
|
||||
| `inactiveHostname` | - | Inactive hostname (e.g., "inactive-api.example.com") - explicit hostname |
|
||||
|
||||
### Deployment
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| ---------------------- | ------- | --------------------------------- |
|
||||
| `circuitBreaker` | `true` | Enable deployment circuit breaker |
|
||||
| `enableExecuteCommand` | `true` | Enable ECS Exec |
|
||||
|
||||
## Mixed Capacity Strategy Explained
|
||||
|
||||
### How Task Distribution Works
|
||||
|
||||
```
|
||||
capacityProviderStrategy: [
|
||||
[capacityProvider: "my-cluster-ec2", base: 2, weight: 3],
|
||||
[capacityProvider: "FARGATE_SPOT", weight: 1],
|
||||
]
|
||||
```
|
||||
|
||||
**Distribution:**
|
||||
|
||||
1. **Base tasks** go to their designated provider first
|
||||
2. **Additional tasks** are distributed by weight ratio
|
||||
|
||||
**Example with 10 tasks:**
|
||||
|
||||
```
|
||||
Tasks 1-2: EC2 (base)
|
||||
Tasks 3-8: EC2 (weight 3 = 75%)
|
||||
Tasks 9-10: FARGATE_SPOT (weight 1 = 25%)
|
||||
```
|
||||
|
||||
### Recommended Strategies
|
||||
|
||||
**Development (Cost Optimized):**
|
||||
|
||||
```groovy
|
||||
capacityProviderStrategy: [
|
||||
[capacityProvider: "FARGATE_SPOT", weight: 1],
|
||||
]
|
||||
```
|
||||
|
||||
**Staging (Balanced):**
|
||||
|
||||
```groovy
|
||||
capacityProviderStrategy: [
|
||||
[capacityProvider: "my-cluster-staging-ec2", base: 1, weight: 2],
|
||||
[capacityProvider: "FARGATE_SPOT", weight: 1],
|
||||
]
|
||||
```
|
||||
|
||||
**Production (Reliability + Burst):**
|
||||
|
||||
```groovy
|
||||
capacityProviderStrategy: [
|
||||
[capacityProvider: "my-cluster-prod-ec2", base: 2, weight: 3],
|
||||
[capacityProvider: "FARGATE_SPOT", weight: 1],
|
||||
]
|
||||
```
|
||||
|
||||
## Building Docker Images
|
||||
|
||||
The pipeline automatically builds and pushes Docker images if a `Dockerfile` exists:
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ... other params ...
|
||||
|
||||
// Image will be built and pushed automatically
|
||||
serviceName: "my-api", // Used as image name
|
||||
imageTag: env.BUILD_NUMBER, // Or defaults to Git SHA
|
||||
|
||||
// Optional: customize build
|
||||
dockerfile: "Dockerfile.prod",
|
||||
dockerContext: "./app",
|
||||
dockerBuildArgs: [
|
||||
NODE_ENV: "production",
|
||||
BUILD_DATE: new Date().format("yyyy-MM-dd")
|
||||
],
|
||||
|
||||
// Or skip build and use existing image
|
||||
buildImage: false,
|
||||
image: "nexus.kodeniks.com/docker-hosted/my-api:v1.2.3",
|
||||
)
|
||||
```
|
||||
|
||||
## Environment Variables and Secrets
|
||||
|
||||
### Plain Environment Variables
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
environment_vars: [
|
||||
NODE_ENV: "production",
|
||||
LOG_LEVEL: "info",
|
||||
API_URL: "https://api.example.com"
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
### Secrets from Secrets Manager
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
secrets: [
|
||||
DATABASE_URL: "arn:aws:secretsmanager:ca-central-1:123456789:secret:my-db-creds",
|
||||
API_KEY: "arn:aws:secretsmanager:ca-central-1:123456789:secret:api-keys::api_key",
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
**Format:** `"ENV_VAR_NAME": "secret-arn"` or `"ENV_VAR_NAME": "secret-arn::json-key"`
|
||||
|
||||
## Auto-Scaling Behavior
|
||||
|
||||
### CPU-Based Scaling
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
minCapacity: 2,
|
||||
maxCapacity: 20,
|
||||
targetCpuUtilization: 70,
|
||||
)
|
||||
```
|
||||
|
||||
- Scales up when average CPU > 70%
|
||||
- Scales down when average CPU < 70%
|
||||
- Cooldown: 60s scale-out, 300s scale-in
|
||||
|
||||
### Request-Based Scaling
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
minCapacity: 2,
|
||||
maxCapacity: 20,
|
||||
targetRequestsPerTarget: 1000, // 1000 requests/minute per task
|
||||
)
|
||||
```
|
||||
|
||||
### Combined Scaling
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
minCapacity: 2,
|
||||
maxCapacity: 20,
|
||||
targetCpuUtilization: 70,
|
||||
targetMemoryUtilization: 80,
|
||||
targetRequestsPerTarget: 1000,
|
||||
)
|
||||
```
|
||||
|
||||
All policies run independently - whichever triggers first wins.
|
||||
|
||||
## Deployment Circuit Breaker
|
||||
|
||||
Enabled by default. If a deployment fails:
|
||||
|
||||
1. ECS detects unhealthy tasks
|
||||
2. After threshold failures, deployment stops
|
||||
3. Automatic rollback to previous version
|
||||
4. CloudWatch alarm triggered
|
||||
|
||||
Disable if needed:
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
circuitBreaker: false,
|
||||
)
|
||||
```
|
||||
|
||||
## ECS Exec (Debugging)
|
||||
|
||||
Enabled by default. Connect to running containers:
|
||||
|
||||
```bash
|
||||
aws ecs execute-command \
|
||||
--cluster my-cluster-dev \
|
||||
--task <task-id> \
|
||||
--container my-api \
|
||||
--interactive \
|
||||
--command "/bin/sh"
|
||||
```
|
||||
|
||||
## Stack Outputs
|
||||
|
||||
| Output | Export Name | Description |
|
||||
| ------------------- | --------------------------------- | -------------------- |
|
||||
| `ServiceName` | `{stackName}-service-name` | ECS service name |
|
||||
| `ServiceArn` | `{stackName}-service-arn` | ECS service ARN |
|
||||
| `TaskDefinitionArn` | `{stackName}-task-definition-arn` | Task definition ARN |
|
||||
| `LogGroupName` | `{stackName}-log-group` | CloudWatch log group |
|
||||
| `TargetGroupArn` | `{stackName}-target-group-arn` | ALB target group ARN |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Tasks Stuck in PENDING
|
||||
|
||||
**Cause:** No capacity available
|
||||
|
||||
**Solutions:**
|
||||
|
||||
1. Check EC2 instances have room for tasks
|
||||
2. Verify Fargate is in capacity strategy
|
||||
3. Check task CPU/memory fits available resources
|
||||
|
||||
### Tasks Failing Health Checks
|
||||
|
||||
```bash
|
||||
# Check container logs
|
||||
aws logs tail /ecs/my-api --follow
|
||||
|
||||
# Check target group health
|
||||
aws elbv2 describe-target-health --target-group-arn <arn>
|
||||
```
|
||||
|
||||
### Deployment Rolling Back
|
||||
|
||||
```bash
|
||||
# Check deployment events
|
||||
aws ecs describe-services --cluster my-cluster --services my-api
|
||||
|
||||
# Check task stopped reasons
|
||||
aws ecs describe-tasks --cluster my-cluster --tasks <task-id>
|
||||
```
|
||||
|
||||
### ECS Exec Not Working
|
||||
|
||||
1. Verify `enableExecuteCommand: true`
|
||||
2. Check task role has SSM permissions
|
||||
3. Ensure SSM agent is running in container
|
||||
|
||||
## Blue/Green Deployments
|
||||
|
||||
Blue/Green deployments provide zero-downtime releases with instant rollback capability.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Two Services**: `myapp-blue` and `myapp-green` run simultaneously
|
||||
2. **DNS Records**: Both active and inactive hostnames point to the same ALB (DNS never changes)
|
||||
3. **ALB Listener Rules**: Traffic routing is controlled by listener rule priorities, not DNS
|
||||
4. **Deploy to Inactive**: New version deploys to inactive service with higher priority rule
|
||||
5. **Test Inactive**: Run integration tests against inactive hostname (same ALB, different rule)
|
||||
6. **Swap Hostnames**: Update listener rule priorities to swap active/inactive (no DNS changes)
|
||||
7. **Keep for Rollback**: Old version stays running for rollback window
|
||||
|
||||
**Important**: DNS records are created once and never change. Both `api.example.com` and `inactive-api.example.com` always resolve to the same ALB. Traffic routing is controlled entirely by ALB listener rule priorities:
|
||||
|
||||
- Active service: Lower priority (e.g., 100) for active hostname
|
||||
- Inactive service: Higher priority (e.g., 200) for inactive hostname
|
||||
- When swapping: Only the listener rule priorities are updated via CDK deployment
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ ALB │
|
||||
│ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ api.example │ │ inactive-api│ │
|
||||
│ │ Priority: │ │ Priority: │ │
|
||||
│ │ 100 │ │ 200 │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ │
|
||||
└─────────┼────────────────┼──────────┘
|
||||
│ │
|
||||
┌───────────────▼──┐ ┌──────▼───────────────┐
|
||||
│ BLUE Service │ │ GREEN Service │
|
||||
│ (v1 - Active) │ │ (v2 - Inactive) │
|
||||
│ ████████████ │ │ ████████████ │
|
||||
│ ████████████ │ │ ████████████ │
|
||||
└──────────────────┘ └──────────────────────┘
|
||||
```
|
||||
|
||||
### Enable Blue/Green
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
spicyECSService(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "my-api-prod",
|
||||
serviceName: "my-api",
|
||||
|
||||
// Cluster info - VPC details auto-import from cluster/VPC stack exports
|
||||
clusterName: "my-cluster-prod",
|
||||
vpcStackName: "my-vpc", // Optional: auto-imported from cluster stack if not provided
|
||||
|
||||
// Container
|
||||
image: "nexus.kodeniks.com/docker-hosted/my-api:latest",
|
||||
containerPort: 3000,
|
||||
|
||||
// Enable Blue/Green
|
||||
blueGreen: true,
|
||||
hostName: "api.example.com", // Auto-generates active/inactive hostnames
|
||||
bgHostedZoneId: "Z1234567890", // Route53 hosted zone (optional, for automatic DNS)
|
||||
|
||||
// ALB Configuration
|
||||
useClusterAlb: false, // Use individual ALB (required for blue/green with individual ALB)
|
||||
albScheme: "internet-facing",
|
||||
certificateArn: "arn:aws:acm:ca-central-1:123456789:certificate/xxx",
|
||||
redirectHttpToHttps: true,
|
||||
healthCheckPath: "/health",
|
||||
priority: 100,
|
||||
|
||||
// Tags
|
||||
ownerTag: "Platform",
|
||||
productTag: "my-product",
|
||||
componentTag: "api",
|
||||
environment: "prod",
|
||||
|
||||
// Test inactive before swap
|
||||
blueGreenTest: { args, buildInfo ->
|
||||
sh "curl -f https://${buildInfo.inactiveHostname}/health"
|
||||
sh "./run-integration-tests.sh ${buildInfo.inactiveHostname}"
|
||||
},
|
||||
|
||||
// Test active after swap
|
||||
smokeTest: { args, buildInfo ->
|
||||
sh "curl -f https://${buildInfo.activeHostname}/health"
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Blue/Green Parameters
|
||||
|
||||
| Parameter | Default | Description |
|
||||
| --------------------- | ------- | -------------------------------------------------------------------------- |
|
||||
| `blueGreen` | `false` | Enable blue/green deployment |
|
||||
| `hostName` | - | Simple hostname (e.g., "api.example.com") - auto-generates active/inactive |
|
||||
| `activeHostname` | - | Active hostname (e.g., "api.example.com") - explicit hostname |
|
||||
| `inactiveHostname` | - | Inactive hostname (e.g., "inactive-api.example.com") - explicit hostname |
|
||||
| `bgHostedZoneId` | - | Route53 hosted zone ID for automatic DNS records (optional) |
|
||||
| `rollbackWindowHours` | `2` | Hours to keep old version for rollback |
|
||||
|
||||
### Rollback
|
||||
|
||||
Instant rollback by swapping hostnames - no new deployment needed:
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
spicyRollback(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "my-api-prod",
|
||||
serviceName: "my-api",
|
||||
|
||||
// Same params as deploy
|
||||
clusterName: "my-cluster-prod",
|
||||
vpcStackName: "my-vpc", // Optional: auto-imported from cluster stack if not provided
|
||||
|
||||
hostName: "api.example.com", // Or use activeHostname/inactiveHostname
|
||||
bgHostedZoneId: "Z1234567890", // If using Route53
|
||||
|
||||
ownerTag: "Platform",
|
||||
productTag: "my-product",
|
||||
componentTag: "api",
|
||||
environment: "prod",
|
||||
)
|
||||
```
|
||||
|
||||
**Rollback time: ~30 seconds** (just ALB rule updates, no DNS propagation delays)
|
||||
|
||||
**How it works:**
|
||||
|
||||
- DNS records for both active and inactive hostnames already exist and point to the same ALB
|
||||
- Rollback simply updates ALB listener rule priorities (e.g., swap priority 100 ↔ 200)
|
||||
- No DNS changes are needed, so rollback is instant
|
||||
|
||||
### State Management
|
||||
|
||||
Active color is stored in SSM Parameter Store:
|
||||
|
||||
```
|
||||
/spicy/{serviceName}/active-color = "blue" | "green"
|
||||
```
|
||||
|
||||
Check current state:
|
||||
|
||||
```bash
|
||||
aws ssm get-parameter --name /spicy/my-api/active-color --query 'Parameter.Value'
|
||||
```
|
||||
|
||||
### Testing Blue/Green DNS
|
||||
|
||||
To verify blue/green DNS configuration is working correctly:
|
||||
|
||||
1. **Verify DNS Records**:
|
||||
|
||||
```bash
|
||||
# Both hostnames should resolve to the same ALB
|
||||
dig api.example.com
|
||||
dig inactive-api.example.com
|
||||
# Both should return the same ALB DNS name
|
||||
```
|
||||
|
||||
2. **Verify ALB Listener Rules**:
|
||||
|
||||
```bash
|
||||
# Get ALB ARN from stack outputs
|
||||
ALB_ARN=$(aws cloudformation describe-stacks --stack-name my-api-prod-alb \
|
||||
--query 'Stacks[0].Outputs[?OutputKey==`LoadBalancerArn`].OutputValue' --output text)
|
||||
|
||||
# Get listener ARN
|
||||
LISTENER_ARN=$(aws cloudformation describe-stacks --stack-name my-api-prod-alb \
|
||||
--query 'Stacks[0].Outputs[?OutputKey==`HTTPSListenerArn`].OutputValue' --output text)
|
||||
|
||||
# Check listener rules
|
||||
aws elbv2 describe-rules --listener-arn $LISTENER_ARN
|
||||
# Active service should have priority 100 for api.example.com
|
||||
# Inactive service should have priority 200 for inactive-api.example.com
|
||||
```
|
||||
|
||||
3. **Test Traffic Routing**:
|
||||
|
||||
```bash
|
||||
# Get ALB DNS name
|
||||
ALB_DNS=$(aws cloudformation describe-stacks --stack-name my-api-prod-alb \
|
||||
--query 'Stacks[0].Outputs[?OutputKey==`LoadBalancerDNS`].OutputValue' --output text)
|
||||
|
||||
# Test active hostname (should hit active service)
|
||||
curl -H "Host: api.example.com" https://$ALB_DNS/health
|
||||
|
||||
# Test inactive hostname (should hit inactive service)
|
||||
curl -H "Host: inactive-api.example.com" https://$ALB_DNS/health
|
||||
```
|
||||
|
||||
4. **Verify After Swap**:
|
||||
|
||||
```bash
|
||||
# After swapping, priorities should be reversed
|
||||
# New active should have priority 100
|
||||
# Old active should have priority 200
|
||||
aws elbv2 describe-rules --listener-arn $LISTENER_ARN
|
||||
```
|
||||
|
||||
**Important**: DNS records never change - they always point to the same ALB. Only listener rule priorities change during swaps.
|
||||
|
||||
---
|
||||
|
||||
## Pipeline Hooks
|
||||
|
||||
Customize pipeline behavior at every stage with Groovy closures.
|
||||
|
||||
### Hook Execution Order
|
||||
|
||||
```
|
||||
┌─────────────┐
|
||||
│ Checkout │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐ ┌──────────────┐
|
||||
│ Build Image │────▶│ onPostBuild │ ← Unit tests, linting
|
||||
└──────┬──────┘ └──────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ onPreDeploy │ ← Setup, integration test prep
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Deploy │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼ (Blue/Green only)
|
||||
┌───────────────┐
|
||||
│ blueGreenTest │ ← Test inactive stack
|
||||
└───────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ Swap (B/G) │
|
||||
└──────┬──────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ onPostDeploy │ ← Cleanup, notifications
|
||||
└──────┬───────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ smokeTest │ ← Smoke tests
|
||||
└─────────────┘
|
||||
```
|
||||
|
||||
### Available Hooks
|
||||
|
||||
#### `buildCommand`
|
||||
|
||||
Custom build command (replaces default `docker build`):
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
buildCommand: { args, buildInfo ->
|
||||
sh "make build"
|
||||
sh "docker tag my-app:latest ${args.image}"
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
#### `onPostBuild`
|
||||
|
||||
After build completes (linting, unit tests):
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
onPostBuild: { args, buildInfo ->
|
||||
sh "npm run lint"
|
||||
sh "npm run test:unit"
|
||||
junit 'coverage/junit.xml'
|
||||
publishHTML(target: [
|
||||
reportDir: 'coverage/lcov-report',
|
||||
reportFiles: 'index.html',
|
||||
reportName: 'Coverage'
|
||||
])
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
#### `onPreDeploy`
|
||||
|
||||
Before deployment (setup, test prep):
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
onPreDeploy: { args, buildInfo ->
|
||||
sh "docker-compose -f docker-compose.test.yml up -d"
|
||||
sh "./wait-for-services.sh"
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
#### `blueGreenTest`
|
||||
|
||||
After inactive stack is up (integration tests):
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
blueGreen: true,
|
||||
blueGreenTest: { args, buildInfo ->
|
||||
echo "Testing inactive: ${buildInfo.inactiveHostname}"
|
||||
sh "curl -f https://${buildInfo.inactiveHostname}/health"
|
||||
sh "./run-integration-tests.sh ${buildInfo.inactiveHostname}"
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
**Available in `buildInfo`:**
|
||||
|
||||
- `inactiveHostname` - Inactive service hostname
|
||||
- `activeHostname` - Active service hostname
|
||||
- `currentActive` - Current active color (blue/green)
|
||||
- `targetColor` - Deployment target color
|
||||
|
||||
#### `onPostDeploy`
|
||||
|
||||
After deployment succeeds (cleanup, notifications):
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
onPostDeploy: { args, buildInfo ->
|
||||
sh "docker-compose -f docker-compose.test.yml down"
|
||||
slackSend(channel: '#deploys', message: "Deployed ${args.serviceName}")
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
#### `smokeTest`
|
||||
|
||||
After deployment or swap (smoke tests):
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
smokeTest: { args, buildInfo ->
|
||||
sh "curl -f https://${buildInfo.activeHostname}/health"
|
||||
sh "curl -f https://${buildInfo.activeHostname}/api/status"
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
### Hook Parameters
|
||||
|
||||
All hooks receive:
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ----------- | ---- | ------------------------- |
|
||||
| `args` | Map | All pipeline arguments |
|
||||
| `buildInfo` | Map | Build context (see below) |
|
||||
|
||||
**`buildInfo` contents:**
|
||||
|
||||
| Key | Description |
|
||||
| ------------------ | ------------------------------- |
|
||||
| `commitSha` | Git commit SHA |
|
||||
| `branch` | Git branch name |
|
||||
| `image` | Built Docker image URI |
|
||||
| `imageTag` | Docker image tag |
|
||||
| `activeHostname` | Active service hostname |
|
||||
| `inactiveHostname` | Inactive service hostname (B/G) |
|
||||
| `currentActive` | Current active color (B/G) |
|
||||
| `targetColor` | Deployment target color (B/G) |
|
||||
| `healthCheckPath` | Health check path |
|
||||
|
||||
---
|
||||
|
||||
## Log Streaming
|
||||
|
||||
The pipeline automatically streams ECS logs during deployment:
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
streamLogs: true, // Default: true
|
||||
)
|
||||
```
|
||||
|
||||
Logs show:
|
||||
|
||||
- Container startup logs
|
||||
- Health check results
|
||||
- Recent ECS service events
|
||||
|
||||
Disable if not needed:
|
||||
|
||||
```groovy
|
||||
spicyECSService(
|
||||
// ...
|
||||
streamLogs: false,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Migration from Legacy
|
||||
|
||||
| Legacy Parameter | New Parameter |
|
||||
| --------------------------------- | --------------------------------------------------- |
|
||||
| `desiredCount` | `desiredCount` |
|
||||
| `minCapacity` | `minCapacity` |
|
||||
| `maxCapacity` | `maxCapacity` |
|
||||
| `cpu` | `cpu` |
|
||||
| `memory` | `memory` |
|
||||
| `containerPort` | `containerPort` |
|
||||
| `healthCheckUrl` | `healthCheckPath` |
|
||||
| `albPriority` | `priority` |
|
||||
| `albScheme: internet-facing` | `useClusterAlb: true, albScheme: "internet-facing"` |
|
||||
| `albScheme: internal` | `useClusterAlb: true, albScheme: "internal"` |
|
||||
| `targetGroupStickinessEnabled` | `stickiness` |
|
||||
| `targetGroupLBCookieDurationSecs` | `stickinessDuration` |
|
||||
| `vpcId`, `vpcCidrBlock`, etc. | `clusterName`, `vpcStackName` (auto-imports) |
|
||||
|
||||
### Key Differences
|
||||
|
||||
1. **CloudFormation imports** - VPC details auto-import from cluster/VPC stack exports (no explicit IDs needed)
|
||||
2. **Individual ALB support** - Set `useClusterAlb: false` for dedicated ALB per service
|
||||
3. **No Ansible** - Pure CDK deployment
|
||||
4. **Capacity providers** - Native mixed EC2/Fargate support
|
||||
5. **Circuit breaker** - Native deployment rollback
|
||||
6. **ECS Exec** - Built-in debugging support
|
||||
7. **Blue/Green hostnames** - Use `hostName` for simple setup or `activeHostname`/`inactiveHostname` for explicit control
|
||||
46
docs/JENKINSFILE_EXAMPLE_NPM.md
Normal file
46
docs/JENKINSFILE_EXAMPLE_NPM.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# Example Jenkinsfile for NPM Package Publishing
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Create a `Jenkinsfile` in your repository root:
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
publishNpmPackage()
|
||||
```
|
||||
|
||||
## With Scoped Package
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
publishNpmPackage(
|
||||
packageName: '@kodeniks/my-package',
|
||||
)
|
||||
```
|
||||
|
||||
## With Custom Options
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
publishNpmPackage(
|
||||
packageName: '@kodeniks/my-package',
|
||||
buildScript: 'build.sh',
|
||||
versionScript: './scripts/docker-ensure-version.sh',
|
||||
registry: 'nexus.kodeniks.com',
|
||||
repository: 'npm-hosted',
|
||||
credentialsId: 'kodeniks-nexus-repository',
|
||||
agentLabel: 'docker',
|
||||
nodeImage: 'node:24-alpine',
|
||||
)
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **No comments at the top** - Jenkinsfiles should start directly with `@Library` or the function call
|
||||
2. **Package name is optional** - It will be read from `package.json` if not provided
|
||||
3. **Scoped packages are supported** - Use format `@scope/package-name`
|
||||
4. **Requires Docker agent** - The agent must have Docker available to run Node containers
|
||||
|
||||
497
docs/JENKINS_UTILITIES.md
Normal file
497
docs/JENKINS_UTILITIES.md
Normal file
@@ -0,0 +1,497 @@
|
||||
# Jenkins Utilities Reference
|
||||
|
||||
All available utility functions in the `spicy-automation` Jenkins shared library.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [cdkUtils](#cdkutils) - CDK command execution
|
||||
- [dockerUtils](#dockerutils) - Docker/Nexus operations
|
||||
- [gitUtils](#gitutils) - Git information
|
||||
- [giteaUtils](#giteautils) - Gitea commit status
|
||||
- [awsUtils](#awsutils) - AWS CLI operations
|
||||
- [spicyUtils](#spicyutils) - Pipeline utilities
|
||||
- [accounts](#accounts) - Account configurations
|
||||
- [manualApproval](#manualapproval) - Approval gates
|
||||
|
||||
---
|
||||
|
||||
## cdkUtils
|
||||
|
||||
Execute AWS CDK commands from Jenkins pipelines.
|
||||
|
||||
### `cdkUtils.deploy(Map args)`
|
||||
|
||||
Deploy a CDK stack.
|
||||
|
||||
```groovy
|
||||
cdkUtils.deploy(
|
||||
account: [
|
||||
region: "ca-central-1",
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
accountId: "123456789012" // optional
|
||||
],
|
||||
stackName: "my-stack",
|
||||
stackType: "vpc", // vpc, ecs-cluster, ecs-service
|
||||
context: [ // Additional CDK context
|
||||
ownerTag: "MyTeam",
|
||||
productTag: "myapp"
|
||||
],
|
||||
workDir: "." // optional, defaults to current dir
|
||||
)
|
||||
```
|
||||
|
||||
### `cdkUtils.diff(Map args)`
|
||||
|
||||
Show changes between deployed and local stack.
|
||||
|
||||
```groovy
|
||||
cdkUtils.diff(
|
||||
account: account,
|
||||
stackName: "my-stack",
|
||||
stackType: "vpc",
|
||||
context: context
|
||||
)
|
||||
```
|
||||
|
||||
### `cdkUtils.synth(Map args)`
|
||||
|
||||
Synthesize CloudFormation template.
|
||||
|
||||
```groovy
|
||||
cdkUtils.synth(
|
||||
account: account,
|
||||
stackName: "my-stack",
|
||||
stackType: "vpc",
|
||||
context: context
|
||||
)
|
||||
```
|
||||
|
||||
### `cdkUtils.destroy(Map args)`
|
||||
|
||||
Destroy a CDK stack.
|
||||
|
||||
```groovy
|
||||
cdkUtils.destroy(
|
||||
account: account,
|
||||
stackName: "my-stack",
|
||||
stackType: "vpc",
|
||||
context: context
|
||||
)
|
||||
```
|
||||
|
||||
### `cdkUtils.install(Map args)`
|
||||
|
||||
Install CDK dependencies.
|
||||
|
||||
```groovy
|
||||
cdkUtils.install(workDir: ".")
|
||||
```
|
||||
|
||||
### `cdkUtils.build(Map args)`
|
||||
|
||||
Build CDK TypeScript (optional - ts-node runs directly).
|
||||
|
||||
```groovy
|
||||
cdkUtils.build(workDir: ".")
|
||||
```
|
||||
|
||||
### `cdkUtils.test(Map args)`
|
||||
|
||||
Run CDK tests.
|
||||
|
||||
```groovy
|
||||
cdkUtils.test(workDir: ".")
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## dockerUtils
|
||||
|
||||
Docker operations with Nexus registry support.
|
||||
|
||||
**Default Configuration:**
|
||||
|
||||
- Registry: `nexus.kodeniks.com`
|
||||
- Repository: `docker-hosted`
|
||||
- Credentials: `kodeniks-nexus-repository`
|
||||
|
||||
### `dockerUtils.buildAndPush(Map args)`
|
||||
|
||||
Build and push a Docker image to Nexus.
|
||||
|
||||
```groovy
|
||||
def imageRef = dockerUtils.buildAndPush(
|
||||
imageName: "my-service", // required
|
||||
dockerfile: "Dockerfile", // default: Dockerfile
|
||||
context: ".", // default: .
|
||||
buildArgs: [ // optional
|
||||
NODE_ENV: "production",
|
||||
VERSION: "1.0.0"
|
||||
],
|
||||
tagLatest: true, // default: true on main branch
|
||||
compareWithLatest: true, // default: true, skip if unchanged
|
||||
registry: "nexus.kodeniks.com", // optional override
|
||||
repository: "docker-hosted", // optional override
|
||||
credentialsId: "kodeniks-nexus-repository" // optional override
|
||||
)
|
||||
|
||||
echo "Pushed: ${imageRef}"
|
||||
// Output: nexus.kodeniks.com/docker-hosted/my-service:abc1234
|
||||
```
|
||||
|
||||
### `dockerUtils.buildImage(Map args)`
|
||||
|
||||
Build a Docker image without pushing.
|
||||
|
||||
```groovy
|
||||
def containerName = dockerUtils.buildImage(
|
||||
imageName: "my-service",
|
||||
dockerfile: "Dockerfile",
|
||||
context: ".",
|
||||
buildArgs: [NODE_ENV: "production"]
|
||||
)
|
||||
```
|
||||
|
||||
### `dockerUtils.tagImage(Map args)`
|
||||
|
||||
Tag an image for Nexus registry.
|
||||
|
||||
```groovy
|
||||
dockerUtils.tagImage(
|
||||
imageName: "my-service",
|
||||
tagLatest: true // also tag as 'latest'
|
||||
)
|
||||
```
|
||||
|
||||
### `dockerUtils.pushImage(Map args)`
|
||||
|
||||
Push image to Nexus with smart comparison.
|
||||
|
||||
```groovy
|
||||
def pushed = dockerUtils.pushImage(
|
||||
imageName: "my-service",
|
||||
tagLatest: true,
|
||||
compareWithLatest: true // skip if identical to remote latest
|
||||
)
|
||||
|
||||
if (pushed) {
|
||||
echo "Image was pushed"
|
||||
} else {
|
||||
echo "Image unchanged, push skipped"
|
||||
}
|
||||
```
|
||||
|
||||
### `dockerUtils.pullImage(Map args)`
|
||||
|
||||
Pull an image from Nexus.
|
||||
|
||||
```groovy
|
||||
def imageRef = dockerUtils.pullImage(
|
||||
imageName: "my-service",
|
||||
tag: "latest" // default: latest
|
||||
)
|
||||
```
|
||||
|
||||
### `dockerUtils.login(Map args)`
|
||||
|
||||
Login to Nexus registry.
|
||||
|
||||
```groovy
|
||||
dockerUtils.login()
|
||||
// Or with custom registry:
|
||||
dockerUtils.login(registry: "other-nexus.example.com")
|
||||
```
|
||||
|
||||
### `dockerUtils.prune()`
|
||||
|
||||
Clean up dangling Docker images.
|
||||
|
||||
```groovy
|
||||
dockerUtils.prune()
|
||||
```
|
||||
|
||||
### `dockerUtils.copyFromImage(Map args)`
|
||||
|
||||
Copy files from a Docker image.
|
||||
|
||||
```groovy
|
||||
dockerUtils.copyFromImage(
|
||||
imageID: "my-image:latest",
|
||||
dockerPath: "/app/dist",
|
||||
jenkinsPath: "./dist"
|
||||
)
|
||||
```
|
||||
|
||||
### `dockerUtils.getImageRefSHA(Map args)`
|
||||
|
||||
Get full image reference with SHA tag.
|
||||
|
||||
```groovy
|
||||
def ref = dockerUtils.getImageRefSHA(imageName: "my-service")
|
||||
// Returns: nexus.kodeniks.com/docker-hosted/my-service:abc1234
|
||||
```
|
||||
|
||||
### `dockerUtils.getImageRefLatest(Map args)`
|
||||
|
||||
Get full image reference with latest tag.
|
||||
|
||||
```groovy
|
||||
def ref = dockerUtils.getImageRefLatest(imageName: "my-service")
|
||||
// Returns: nexus.kodeniks.com/docker-hosted/my-service:latest
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## gitUtils
|
||||
|
||||
Git repository information.
|
||||
|
||||
### `gitUtils.getSHA()`
|
||||
|
||||
Get full commit SHA.
|
||||
|
||||
```groovy
|
||||
def sha = gitUtils.getSHA()
|
||||
// Returns: abc123def456...
|
||||
```
|
||||
|
||||
### `gitUtils.getShortSHA()`
|
||||
|
||||
Get short commit SHA (7 characters).
|
||||
|
||||
```groovy
|
||||
def shortSha = gitUtils.getShortSHA()
|
||||
// Returns: abc123d
|
||||
```
|
||||
|
||||
### `gitUtils.getBranch()`
|
||||
|
||||
Get current branch name.
|
||||
|
||||
```groovy
|
||||
def branch = gitUtils.getBranch()
|
||||
// Returns: main, feature/xyz, etc.
|
||||
```
|
||||
|
||||
### `gitUtils.isMain()`
|
||||
|
||||
Check if on main branch.
|
||||
|
||||
```groovy
|
||||
if (gitUtils.isMain()) {
|
||||
echo "On main branch - deploying!"
|
||||
}
|
||||
```
|
||||
|
||||
### `gitUtils.getRemoteURL()`
|
||||
|
||||
Get Git remote URL.
|
||||
|
||||
```groovy
|
||||
def url = gitUtils.getRemoteURL()
|
||||
// Returns: git@git.kodeniks.com:CORP/my-repo.git
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## giteaUtils
|
||||
|
||||
Update Gitea commit status.
|
||||
|
||||
**Default Configuration:**
|
||||
|
||||
- Gitea URL: `https://git.kodeniks.com`
|
||||
- Credentials: `kodeniks-gitea-token`
|
||||
|
||||
### `giteaUtils.setSuccess(context)`
|
||||
|
||||
Set commit status to success.
|
||||
|
||||
```groovy
|
||||
giteaUtils.setSuccess("build")
|
||||
giteaUtils.setSuccess("deploy")
|
||||
```
|
||||
|
||||
### `giteaUtils.setPending(context)`
|
||||
|
||||
Set commit status to pending.
|
||||
|
||||
```groovy
|
||||
giteaUtils.setPending("build")
|
||||
```
|
||||
|
||||
### `giteaUtils.setFailed(context)`
|
||||
|
||||
Set commit status to failed.
|
||||
|
||||
```groovy
|
||||
giteaUtils.setFailed("build")
|
||||
```
|
||||
|
||||
### `giteaUtils.setError(context)`
|
||||
|
||||
Set commit status to error.
|
||||
|
||||
```groovy
|
||||
giteaUtils.setError("build")
|
||||
```
|
||||
|
||||
### `giteaUtils.updateStatus(Map args)`
|
||||
|
||||
Update commit status with full control.
|
||||
|
||||
```groovy
|
||||
giteaUtils.updateStatus(
|
||||
context: "build",
|
||||
state: "success", // success, pending, failure, error
|
||||
message: "Build passed",
|
||||
giteaUrl: "https://git.kodeniks.com", // optional override
|
||||
credentialsId: "kodeniks-gitea-token" // optional override
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## awsUtils
|
||||
|
||||
AWS CLI operations.
|
||||
|
||||
### `awsUtils.assumeRole(Map args)`
|
||||
|
||||
Assume an IAM role.
|
||||
|
||||
```groovy
|
||||
awsUtils.assumeRole(
|
||||
roleArn: "arn:aws:iam::123456789012:role/MyRole",
|
||||
sessionName: "jenkins-deploy"
|
||||
)
|
||||
```
|
||||
|
||||
### `awsUtils.getCallerIdentity(Map args)`
|
||||
|
||||
Get current AWS identity.
|
||||
|
||||
```groovy
|
||||
def identity = awsUtils.getCallerIdentity(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1"
|
||||
)
|
||||
```
|
||||
|
||||
### `awsUtils.describeStacks(Map args)`
|
||||
|
||||
Describe CloudFormation stacks.
|
||||
|
||||
```groovy
|
||||
def stacks = awsUtils.describeStacks(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "my-stack"
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## spicyUtils
|
||||
|
||||
Pipeline utility functions.
|
||||
|
||||
### `spicyUtils.stageWithFailure(name, closure)`
|
||||
|
||||
Run a stage that sets Gitea status on failure.
|
||||
|
||||
```groovy
|
||||
spicyUtils.stageWithFailure("Build") {
|
||||
sh "npm run build"
|
||||
}
|
||||
```
|
||||
|
||||
### `spicyUtils.withRetry(Map args, closure)`
|
||||
|
||||
Retry a block with exponential backoff.
|
||||
|
||||
```groovy
|
||||
spicyUtils.withRetry(maxAttempts: 3, delay: 5) {
|
||||
sh "flaky-command"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## accounts
|
||||
|
||||
Multi-account configuration management.
|
||||
|
||||
### `accounts.get()`
|
||||
|
||||
Get all account configurations.
|
||||
|
||||
```groovy
|
||||
def allAccounts = accounts.get()
|
||||
|
||||
def prodAccount = allAccounts.SPICY_CA_CENRAL_1_PROD
|
||||
def devAccount = allAccounts.SPICY_CA_CENRAL_1_DEV
|
||||
```
|
||||
|
||||
### Account Object Properties
|
||||
|
||||
| Property | Description |
|
||||
| ------------------------- | ------------------------------ |
|
||||
| `accountId` | AWS Account ID |
|
||||
| `region` | AWS Region |
|
||||
| `jenkinsAwsCredentialsId` | Jenkins credential ID |
|
||||
| `vpcStackName` | Default VPC stack name |
|
||||
| `ecsClusterStackName` | Default ECS cluster stack name |
|
||||
|
||||
### Available Accounts
|
||||
|
||||
| Key | Description |
|
||||
| --------------------------- | ----------------------- |
|
||||
| `SPICY_CA_CENRAL_1` | Base account |
|
||||
| `SPICY_CA_CENRAL_1_DEV` | Development environment |
|
||||
| `SPICY_CA_CENRAL_1_SANDBOX` | Sandbox environment |
|
||||
| `SPICY_CA_CENRAL_1_QA` | QA environment |
|
||||
| `SPICY_CA_CENRAL_1_STAGING` | Staging environment |
|
||||
| `SPICY_CA_CENRAL_1_PROD` | Production environment |
|
||||
|
||||
---
|
||||
|
||||
## manualApproval
|
||||
|
||||
Manual approval gates for pipelines.
|
||||
|
||||
### `manualApproval(Map args)`
|
||||
|
||||
Request manual approval before proceeding.
|
||||
|
||||
```groovy
|
||||
manualApproval(
|
||||
message: "Deploy to production?",
|
||||
submitter: "admin,deployers", // optional: allowed approvers
|
||||
timeout: 24, // optional: hours to wait
|
||||
timeoutUnit: "HOURS" // optional: HOURS, MINUTES, DAYS
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## spicyDefaults
|
||||
|
||||
Apply default pipeline configuration.
|
||||
|
||||
### `spicyDefaults(Map args)`
|
||||
|
||||
Merge defaults into pipeline arguments.
|
||||
|
||||
```groovy
|
||||
def call(Map args) {
|
||||
args = spicyDefaults(args)
|
||||
// args now has default pipelineProperties, etc.
|
||||
}
|
||||
```
|
||||
|
||||
Default properties applied:
|
||||
|
||||
- Build discarder (keep 10 builds)
|
||||
- Disable concurrent builds
|
||||
- GitHub project URL (if applicable)
|
||||
208
docs/PERSISTENT_ALB.md
Normal file
208
docs/PERSISTENT_ALB.md
Normal file
@@ -0,0 +1,208 @@
|
||||
# Persistent ALB Pattern (bg-common)
|
||||
|
||||
The persistent ALB pattern ensures that ALB DNS names remain constant even when service stacks are deleted and recreated. This matches the legacy `bg-common` pattern.
|
||||
|
||||
## How It Works
|
||||
|
||||
The bg-common ALB stack is **always created separately**. Services reference it via `albLoadBalancerArn`:
|
||||
|
||||
1. **bg-common ALB stack is deployed separately** (`{serviceStackName}-alb` or via `spicyALB`)
|
||||
2. **The ALB stack persists** across service deployments
|
||||
3. **Service stacks reference the existing ALB** via `albLoadBalancerArn` (maps to `existingALB` in construct)
|
||||
4. **ALB DNS name stays constant** even when service stacks are deleted/recreated
|
||||
|
||||
### Stack Naming
|
||||
|
||||
- **ALB Stack**: `{serviceStackName}-alb` (shared between blue/green)
|
||||
- **Service Stack (Rolling)**: `{serviceStackName}`
|
||||
- **Service Stack (Blue/Green)**: `{serviceStackName}-blue` and `{serviceStackName}-green`
|
||||
|
||||
For blue/green deployments, both blue and green services share the same ALB stack.
|
||||
|
||||
## Usage
|
||||
|
||||
### Rolling Deployment (Non-Blue/Green)
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
spicyECSService(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "my-api-dev",
|
||||
serviceName: "my-api",
|
||||
|
||||
// ... cluster/VPC config ...
|
||||
|
||||
// Container
|
||||
image: "nexus.kodeniks.com/docker-hosted/my-api:latest",
|
||||
containerPort: 3000,
|
||||
|
||||
// Reference bg-common ALB (always created separately) - auto-imports from ALB stack
|
||||
albStackName: "my-api-dev-alb", // Auto-imports ALB ARN and listener ARNs
|
||||
|
||||
// Routing
|
||||
hostHeader: "api-dev.example.com",
|
||||
priority: 100,
|
||||
|
||||
// Tags
|
||||
ownerTag: "MyTeam",
|
||||
productTag: "my-product",
|
||||
componentTag: "api",
|
||||
environment: "dev",
|
||||
)
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
|
||||
1. Pipeline deploys `my-api-dev-alb` stack (ALB)
|
||||
2. Pipeline gets ALB outputs (DNS, listener ARNs)
|
||||
3. Pipeline deploys `my-api-dev` stack (service) referencing the ALB
|
||||
4. ALB DNS name stays constant: `my-api-dev-alb-123456789.ca-central-1.elb.amazonaws.com`
|
||||
|
||||
### Blue/Green Deployment
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
spicyECSService(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "my-api-prod", // Base name (blue/green suffixes added automatically)
|
||||
serviceName: "my-api",
|
||||
|
||||
// ... cluster/VPC config ...
|
||||
|
||||
// Container
|
||||
image: "nexus.kodeniks.com/docker-hosted/my-api:latest",
|
||||
containerPort: 3000,
|
||||
|
||||
// Blue/Green
|
||||
blueGreen: true,
|
||||
activeHostname: "api.example.com",
|
||||
inactiveHostname: "inactive-api.example.com",
|
||||
bgHostedZoneId: "Z1234567890", // Route53 hosted zone
|
||||
|
||||
// Reference bg-common ALB (always created separately) - auto-imports from ALB stack
|
||||
albStackName: "my-api-prod-alb", // Auto-imports ALB ARN and listener ARNs
|
||||
healthCheckPath: "/health",
|
||||
|
||||
// Tags
|
||||
ownerTag: "MyTeam",
|
||||
productTag: "my-product",
|
||||
componentTag: "api",
|
||||
environment: "prod",
|
||||
)
|
||||
```
|
||||
|
||||
**What happens:**
|
||||
|
||||
1. Pipeline deploys `my-api-prod-alb` stack (ALB) - **only once, shared by blue/green**
|
||||
2. Pipeline gets ALB outputs (or uses `albStackName` for auto-import)
|
||||
3. Pipeline deploys `my-api-prod-green` stack (inactive service) referencing the ALB
|
||||
4. Service creates listener rules on **both** HTTP and HTTPS listeners (if both available)
|
||||
5. Pipeline swaps hostname routing via ALB listener rule priorities (no DNS changes)
|
||||
6. ALB DNS name stays constant across all deployments
|
||||
|
||||
**How Blue/Green Routing Works:**
|
||||
|
||||
- **DNS Records**: Both `api.example.com` and `inactive-api.example.com` DNS records are created and always point to the same ALB
|
||||
- **Traffic Routing**: Controlled by ALB listener rule priorities, not DNS:
|
||||
- Active service: Listener rule with priority 100 for `api.example.com`
|
||||
- Inactive service: Listener rule with priority 200 for `inactive-api.example.com`
|
||||
- **Swapping**: When swapping, only the listener rule priorities are updated (via CDK deployment). DNS records never change.
|
||||
- **Result**: Zero-downtime swaps in ~30 seconds (just ALB rule updates, no DNS propagation delays)
|
||||
|
||||
## Benefits
|
||||
|
||||
1. **Static DNS**: ALB DNS name never changes (unless ALB stack is deleted)
|
||||
2. **No DNS Updates**: Cloudflare/Route53 records set once and never need updating
|
||||
3. **Faster Deployments**: ALB stack only updates when ALB config changes
|
||||
4. **Cost Efficient**: ALB persists, avoiding recreation costs
|
||||
|
||||
## ALB Stack Lifecycle
|
||||
|
||||
- **Created**: Deployed separately via `spicyALB` pipeline function or manually
|
||||
- **Updated**: When ALB configuration changes (certificate, scheme, etc.)
|
||||
- **Deleted**: Only when explicitly destroyed (not deleted with service stacks)
|
||||
|
||||
## Stack Outputs
|
||||
|
||||
The ALB stack exports these outputs (used by service stacks via CloudFormation imports):
|
||||
|
||||
| Output Key | Description | Export Name |
|
||||
| ------------------ | ------------------ | -------------------------------------------- |
|
||||
| `LoadBalancerDNS` | ALB DNS name | `{stackName}-internet-facing-url` |
|
||||
| `LoadBalancerArn` | ALB ARN | `{stackName}-internet-facing-arn` |
|
||||
| `HTTPListenerArn` | HTTP listener ARN | `{stackName}-internet-facing-http-listener` |
|
||||
| `HTTPSListenerArn` | HTTPS listener ARN | `{stackName}-internet-facing-https-listener` |
|
||||
|
||||
**Minimal Props:** When using `albStackName` in service stack, these are auto-imported via CloudFormation `Fn::ImportValue`. The service creates listener rules on **both** HTTP and HTTPS listeners (if both are available), matching the old template behavior.
|
||||
| `SecurityGroupId` | ALB security group ID | `{stackName}-internet-facing-security-group` |
|
||||
|
||||
## Deploying the bg-common ALB Stack
|
||||
|
||||
The bg-common ALB stack must be deployed separately before deploying services:
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
// Deploy bg-common ALB stack
|
||||
spicyALB(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "my-api-dev-alb",
|
||||
vpcId: "vpc-12345678",
|
||||
availabilityZones: "ca-central-1a,ca-central-1b",
|
||||
subnetIds: "subnet-xxx,subnet-yyy",
|
||||
scheme: "internet-facing",
|
||||
certificateArn: "arn:aws:acm:...",
|
||||
ownerTag: "MyTeam",
|
||||
productTag: "my-product",
|
||||
componentTag: "alb",
|
||||
environment: "dev",
|
||||
)
|
||||
|
||||
// Then deploy service referencing the ALB
|
||||
spicyECSService(
|
||||
// ... config ...
|
||||
albLoadBalancerArn: "arn:aws:elasticloadbalancing:...", // From ALB stack outputs
|
||||
albHttpsListenerArn: "arn:aws:elasticloadbalancing:...", // From ALB stack outputs
|
||||
)
|
||||
```
|
||||
|
||||
## Comparison with Legacy bg-common
|
||||
|
||||
| Feature | Legacy bg-common | New Persistent ALB |
|
||||
| ------------------- | ---------------------- | ------------------------------------------ |
|
||||
| **Stack Name** | `{envName}-bg-common` | `{serviceStackName}-alb` |
|
||||
| **Deployment** | Separate pipeline step | Separate pipeline step (via `spicyALB`) |
|
||||
| **Blue/Green** | Shared ALB | Shared ALB |
|
||||
| **DNS Management** | Manual | Automatic (Route53) or Manual (Cloudflare) |
|
||||
| **Stack Lifecycle** | Manual | Manual (deployed separately) |
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### ALB Stack Not Found
|
||||
|
||||
If the ALB stack doesn't exist, the pipeline will create it automatically. If you see errors:
|
||||
|
||||
1. Check the ALB stack name: `{serviceStackName}-alb`
|
||||
2. Verify the stack exists: `aws cloudformation describe-stacks --stack-name {stackName}-alb`
|
||||
3. Check stack outputs: `aws cloudformation describe-stacks --stack-name {stackName}-alb --query 'Stacks[0].Outputs'`
|
||||
|
||||
### ALB DNS Name Changed
|
||||
|
||||
This should never happen with the persistent ALB pattern. If it does:
|
||||
|
||||
1. Check if the ALB stack was deleted/recreated
|
||||
2. Verify the ALB stack name is consistent
|
||||
3. Check CloudFormation stack events for ALB replacement
|
||||
|
||||
### Service Can't Find ALB
|
||||
|
||||
If the service stack can't reference the ALB:
|
||||
|
||||
1. Verify ALB stack outputs are available
|
||||
2. Check that `albLoadBalancerArn` is set in the service context
|
||||
3. Verify the ALB ARN is correct
|
||||
317
docs/VPC.md
Normal file
317
docs/VPC.md
Normal file
@@ -0,0 +1,317 @@
|
||||
# VPC Deployment Guide
|
||||
|
||||
Deploy production-ready VPCs with `spicyVPC`.
|
||||
|
||||
## Minimal Setup
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
spicyVPC(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "my-vpc",
|
||||
ownerTag: "MyTeam",
|
||||
productTag: "myproduct",
|
||||
componentTag: "vpc",
|
||||
)
|
||||
```
|
||||
|
||||
This creates a VPC with all defaults - see [What Gets Created](#what-gets-created).
|
||||
|
||||
## Full Configuration
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
spicyVPC(
|
||||
// Required
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "production-vpc",
|
||||
ownerTag: "Platform",
|
||||
productTag: "myapp",
|
||||
componentTag: "network",
|
||||
|
||||
// Optional - VPC Configuration
|
||||
vpcCidr: "10.0.0.0/16", // Default: 172.1.0.0/16
|
||||
vpcTenancy: "default", // default | dedicated
|
||||
numberOfAzs: 4, // 2, 3, or 4
|
||||
availabilityZones: "ca-central-1a,ca-central-1b,ca-central-1c,ca-central-1d",
|
||||
|
||||
// Optional - Subnet Creation
|
||||
createPrivateSubnets: true, // Default: true
|
||||
createAdditionalPrivateSubnets: true, // Default: true (creates NACL-protected subnets)
|
||||
|
||||
// Optional - Subnet Tags
|
||||
publicSubnetTag: "Network=Public",
|
||||
privateSubnetATag: "Network=Private",
|
||||
privateSubnetBTag: "Network=Private",
|
||||
|
||||
// Optional - Custom CIDRs (see Custom CIDR Configuration below)
|
||||
|
||||
// Optional - Pipeline Behavior
|
||||
showDiff: true, // Show diff before deploy (default: true)
|
||||
showDiffOnPR: true, // Show diff on non-main branches (default: true)
|
||||
|
||||
// Optional - Hooks
|
||||
onPreDeploy: { args, ctx -> }, // Called before deployment
|
||||
onPostDeploy: { args, ctx -> }, // Called after deployment
|
||||
)
|
||||
```
|
||||
|
||||
## Parameters Reference
|
||||
|
||||
### Required Parameters
|
||||
|
||||
| Parameter | Type | Description |
|
||||
| ------------------------- | ------ | ------------------------------------------------------------- |
|
||||
| `jenkinsAwsCredentialsId` | string | Jenkins credential ID for AWS access |
|
||||
| `region` | string | AWS region (e.g., `ca-central-1`) |
|
||||
| `stackName` | string | CloudFormation stack name (must be unique per account/region) |
|
||||
| `ownerTag` | string | Owner tag applied to all resources |
|
||||
| `productTag` | string | Product tag applied to all resources |
|
||||
| `componentTag` | string | Component tag applied to all resources |
|
||||
|
||||
### VPC Configuration
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| ------------------- | ------ | -------------- | ------------------------------------------- |
|
||||
| `vpcCidr` | string | `172.1.0.0/16` | VPC CIDR block |
|
||||
| `vpcTenancy` | string | `default` | Instance tenancy (`default` or `dedicated`) |
|
||||
| `numberOfAzs` | number | `4` | Number of availability zones (2, 3, or 4) |
|
||||
| `availabilityZones` | string | auto | Comma-separated AZ list |
|
||||
|
||||
### Subnet Configuration
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| -------------------------------- | ------- | ------- | ------------------------------------------- |
|
||||
| `createPrivateSubnets` | boolean | `true` | Create private subnets with NAT Gateways |
|
||||
| `createAdditionalPrivateSubnets` | boolean | `true` | Create secondary private subnets with NACLs |
|
||||
|
||||
### Subnet Tags
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| ------------------- | ------ | ----------------- | --------------------------------- |
|
||||
| `publicSubnetTag` | string | `Network=Public` | Tag for public subnets |
|
||||
| `privateSubnetATag` | string | `Network=Private` | Tag for primary private subnets |
|
||||
| `privateSubnetBTag` | string | `Network=Private` | Tag for secondary private subnets |
|
||||
|
||||
## What Gets Created
|
||||
|
||||
With default settings, `spicyVPC` creates:
|
||||
|
||||
### Network Resources
|
||||
|
||||
- **1 VPC** with DNS hostnames and DNS support enabled
|
||||
- **1 Internet Gateway** attached to the VPC
|
||||
- **DHCP Options** configured for the region
|
||||
|
||||
### Public Subnets (4 AZs)
|
||||
|
||||
- 4 public subnets (one per AZ)
|
||||
- 1 shared public route table with route to Internet Gateway
|
||||
- Auto-assign public IP enabled
|
||||
|
||||
### Private Subnets Type 1 (4 AZs)
|
||||
|
||||
- 4 private subnets (one per AZ)
|
||||
- 4 NAT Gateways (one per AZ) with Elastic IPs
|
||||
- 4 route tables with routes to NAT Gateways
|
||||
|
||||
### Private Subnets Type 2 (4 AZs)
|
||||
|
||||
- 4 additional private subnets (one per AZ)
|
||||
- 4 Network ACLs (allow all by default, customizable)
|
||||
- 4 route tables with routes to NAT Gateways
|
||||
|
||||
### VPC Endpoints
|
||||
|
||||
- S3 Gateway Endpoint (free, attached to all private route tables)
|
||||
|
||||
## Default CIDR Blocks
|
||||
|
||||
| Subnet | AZ A | AZ B | AZ C | AZ D |
|
||||
| --------- | -------------- | -------------- | -------------- | -------------- |
|
||||
| Public | 172.1.128.0/20 | 172.1.144.0/20 | 172.1.160.0/20 | 172.1.176.0/20 |
|
||||
| Private 1 | 172.1.0.0/19 | 172.1.32.0/19 | 172.1.64.0/19 | 172.1.96.0/19 |
|
||||
| Private 2 | 172.1.192.0/21 | 172.1.200.0/21 | 172.1.208.0/21 | 172.1.216.0/21 |
|
||||
|
||||
## Custom CIDR Configuration
|
||||
|
||||
Override individual subnet CIDRs:
|
||||
|
||||
```groovy
|
||||
spicyVPC(
|
||||
// ... required params ...
|
||||
|
||||
// Public subnets
|
||||
publicSubnetACidr: "10.0.0.0/20",
|
||||
publicSubnetBCidr: "10.0.16.0/20",
|
||||
publicSubnetCCidr: "10.0.32.0/20",
|
||||
publicSubnetDCidr: "10.0.48.0/20",
|
||||
|
||||
// Private subnets (type 1)
|
||||
privateSubnetA1Cidr: "10.0.64.0/19",
|
||||
privateSubnetB1Cidr: "10.0.96.0/19",
|
||||
privateSubnetC1Cidr: "10.0.128.0/19",
|
||||
privateSubnetD1Cidr: "10.0.160.0/19",
|
||||
|
||||
// Private subnets (type 2, with NACLs)
|
||||
privateSubnetA2Cidr: "10.0.192.0/21",
|
||||
privateSubnetB2Cidr: "10.0.200.0/21",
|
||||
privateSubnetC2Cidr: "10.0.208.0/21",
|
||||
privateSubnetD2Cidr: "10.0.216.0/21",
|
||||
)
|
||||
```
|
||||
|
||||
## CloudFormation Outputs
|
||||
|
||||
The stack exports these values for cross-stack references:
|
||||
|
||||
| Export Name | Description |
|
||||
| --------------------------------------- | ------------------------- |
|
||||
| `{stackName}-VPCID` | VPC ID |
|
||||
| `{stackName}-VPCCIDR` | VPC CIDR block |
|
||||
| `{stackName}-PublicSubnetAID` | Public Subnet A ID |
|
||||
| `{stackName}-PublicSubnetBID` | Public Subnet B ID |
|
||||
| `{stackName}-PublicSubnetCID` | Public Subnet C ID |
|
||||
| `{stackName}-PublicSubnetDID` | Public Subnet D ID |
|
||||
| `{stackName}-PrivateSubnetA1ID` | Private Subnet A1 ID |
|
||||
| `{stackName}-PrivateSubnetB1ID` | Private Subnet B1 ID |
|
||||
| `{stackName}-PrivateSubnetC1ID` | Private Subnet C1 ID |
|
||||
| `{stackName}-PrivateSubnetD1ID` | Private Subnet D1 ID |
|
||||
| `{stackName}-PrivateSubnetA2ID` | Private Subnet A2 ID |
|
||||
| `{stackName}-PrivateSubnetB2ID` | Private Subnet B2 ID |
|
||||
| `{stackName}-PrivateSubnetC2ID` | Private Subnet C2 ID |
|
||||
| `{stackName}-PrivateSubnetD2ID` | Private Subnet D2 ID |
|
||||
| `{stackName}-NATZoneAEIP` | NAT Gateway A Elastic IP |
|
||||
| `{stackName}-NATZoneBEIP` | NAT Gateway B Elastic IP |
|
||||
| `{stackName}-NATZoneCEIP` | NAT Gateway C Elastic IP |
|
||||
| `{stackName}-NATZoneDEIP` | NAT Gateway D Elastic IP |
|
||||
| `{stackName}-PublicSubnetRouteTable` | Public Route Table ID |
|
||||
| `{stackName}-PrivateSubnetA1RouteTable` | Private A1 Route Table ID |
|
||||
| `{stackName}-PrivateSubnetB1RouteTable` | Private B1 Route Table ID |
|
||||
| `{stackName}-PrivateSubnetC1RouteTable` | Private C1 Route Table ID |
|
||||
| `{stackName}-PrivateSubnetD1RouteTable` | Private D1 Route Table ID |
|
||||
|
||||
## Using Account Configurations
|
||||
|
||||
For multi-environment deployments:
|
||||
|
||||
```groovy
|
||||
@Library(["spicy-automation@main"]) _
|
||||
|
||||
def account = accounts.get().SPICY_CA_CENRAL_1_PROD
|
||||
|
||||
spicyVPC(
|
||||
jenkinsAwsCredentialsId: account.jenkinsAwsCredentialsId,
|
||||
region: account.region,
|
||||
accountId: account.accountId,
|
||||
stackName: "production-vpc",
|
||||
ownerTag: "Platform",
|
||||
productTag: "spicy",
|
||||
componentTag: "vpc",
|
||||
)
|
||||
```
|
||||
|
||||
## Pipeline Hooks
|
||||
|
||||
### Pre-Deploy Hook
|
||||
|
||||
```groovy
|
||||
spicyVPC(
|
||||
// ... params ...
|
||||
onPreDeploy: { args, ctx ->
|
||||
echo "About to deploy ${args.stackName}"
|
||||
// Run custom validation, notifications, etc.
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
### Post-Deploy Hook
|
||||
|
||||
```groovy
|
||||
spicyVPC(
|
||||
// ... params ...
|
||||
onPostDeploy: { args, ctx ->
|
||||
echo "Deployed stack: ${ctx.stackName}"
|
||||
// Update DNS, notify Slack, etc.
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Development VPC (2 AZs, No NACL Subnets)
|
||||
|
||||
```groovy
|
||||
spicyVPC(
|
||||
jenkinsAwsCredentialsId: "aws-dev",
|
||||
region: "ca-central-1",
|
||||
stackName: "dev-vpc",
|
||||
ownerTag: "Dev",
|
||||
productTag: "myapp",
|
||||
componentTag: "vpc",
|
||||
numberOfAzs: 2,
|
||||
createAdditionalPrivateSubnets: false,
|
||||
)
|
||||
```
|
||||
|
||||
### Production VPC (Full HA)
|
||||
|
||||
```groovy
|
||||
spicyVPC(
|
||||
jenkinsAwsCredentialsId: "aws-prod",
|
||||
region: "ca-central-1",
|
||||
stackName: "prod-vpc",
|
||||
ownerTag: "Platform",
|
||||
productTag: "myapp",
|
||||
componentTag: "vpc",
|
||||
vpcCidr: "10.0.0.0/16",
|
||||
numberOfAzs: 4,
|
||||
createPrivateSubnets: true,
|
||||
createAdditionalPrivateSubnets: true,
|
||||
)
|
||||
```
|
||||
|
||||
### Public-Only VPC (No NAT Gateways)
|
||||
|
||||
```groovy
|
||||
spicyVPC(
|
||||
jenkinsAwsCredentialsId: "aws-credentials",
|
||||
region: "ca-central-1",
|
||||
stackName: "public-vpc",
|
||||
ownerTag: "Dev",
|
||||
productTag: "myapp",
|
||||
componentTag: "vpc",
|
||||
createPrivateSubnets: false,
|
||||
)
|
||||
```
|
||||
|
||||
## Local Testing
|
||||
|
||||
Test VPC synthesis locally:
|
||||
|
||||
```bash
|
||||
cd spicy-automation
|
||||
|
||||
# Synthesize with minimal config
|
||||
npx cdk synth \
|
||||
-c stackType=vpc \
|
||||
-c stackName=test-vpc \
|
||||
-c ownerTag=Test \
|
||||
-c productTag=test \
|
||||
-c componentTag=vpc
|
||||
|
||||
# With custom options
|
||||
npx cdk synth \
|
||||
-c stackType=vpc \
|
||||
-c stackName=test-vpc \
|
||||
-c ownerTag=Test \
|
||||
-c productTag=test \
|
||||
-c componentTag=vpc \
|
||||
-c vpcCidr=10.0.0.0/16 \
|
||||
-c numberOfAzs=2 \
|
||||
-c createAdditionalPrivateSubnets=false
|
||||
```
|
||||
Reference in New Issue
Block a user