Skip to content

Commit b1ed1a1

Browse files
ci(infra): add acm certificate management and enhance CI workflows
1 parent 1bbd692 commit b1ed1a1

8 files changed

Lines changed: 71 additions & 21 deletions

File tree

.github/workflows/deploy_production.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,11 @@ jobs:
277277
- name: Deploy ProductionAppStack
278278
if: steps.infra-changed.outputs.should_deploy == 'true'
279279
working-directory: infra
280+
# Required by infra/lib/config.ts; synth fails fast if unset.
281+
# Locally these come from infra/.env (gitignored).
282+
env:
283+
ROUTE53_ZONE_ID: ${{ secrets.ROUTE53_ZONE_ID }}
284+
CERTIFICATE_ARN: ${{ secrets.CERTIFICATE_ARN }}
280285
run: |
281286
npx cdk deploy ProductionAppStack \
282287
--require-approval never \

.github/workflows/deploy_staging.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,11 @@ jobs:
218218
- name: Deploy StagingAppStack
219219
if: steps.infra-changed.outputs.should_deploy == 'true'
220220
working-directory: infra
221+
# Required by infra/lib/config.ts; synth fails fast if unset.
222+
# Locally these come from infra/.env (gitignored).
223+
env:
224+
ROUTE53_ZONE_ID: ${{ secrets.ROUTE53_ZONE_ID }}
225+
CERTIFICATE_ARN: ${{ secrets.CERTIFICATE_ARN }}
221226
run: |
222227
npx cdk deploy StagingAppStack \
223228
--require-approval never \

Makefile

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ RESET := \033[0m
3535
ROOT_DIR := $(shell pwd)
3636
SERVER_DIR := $(ROOT_DIR)/apps/server
3737
WEB_DIR := $(ROOT_DIR)/apps/web
38+
INFRA_DIR := $(ROOT_DIR)/infra
3839
SERVER_PORT := 8080
3940
WEB_PORT := 3000
4041
# macOS default open-file limit (256) breaks testcontainers; bump for test targets.
@@ -147,7 +148,7 @@ build-web: ## Build web production bundle
147148

148149
##@ Test
149150

150-
test: test-server test-web ## Run unit tests for server + web
151+
test: test-server test-web test-infra ## Run unit tests for server + web + infra
151152

152153
test-server: ## Run Go unit tests (race)
153154
@echo "$(CYAN)Running Go tests with -race...$(RESET)"
@@ -160,9 +161,12 @@ test-server-e2e: ensure-docker ## Run every -tags=e2e package (admin_cli, oidc,
160161
test-web: ## Run web tests (vitest)
161162
cd $(WEB_DIR) && npm run test
162163

164+
test-infra: ## Run CDK infra tests (vitest)
165+
cd $(INFRA_DIR) && npm run test
166+
163167
##@ Lint, Vet, Format
164168

165-
lint: lint-server lint-web ## Run all linters
169+
lint: lint-server lint-web lint-infra ## Run all linters
166170

167171
lint-server: ## golangci-lint on the Go module
168172
@echo "$(CYAN)Linting Go (golangci-lint $(GOLANGCI_LINT_VERSION))...$(RESET)"
@@ -171,19 +175,22 @@ lint-server: ## golangci-lint on the Go module
171175
lint-web: ## Lint the web client
172176
cd $(WEB_DIR) && npm run lint
173177

178+
lint-infra: ## Lint the CDK infra (oxlint)
179+
cd $(INFRA_DIR) && npm run lint
180+
174181
vet: vet-server ## Run `go vet` (fast baseline static analyzers)
175182

176183
vet-server: ## go vet ./... on the Go module
177184
@echo "$(CYAN)Running go vet...$(RESET)"
178185
cd $(SERVER_DIR) && $(GO) vet ./...
179186

180-
fmt: fmt-server fmt-web ## Format server (gofmt+goimports) and web (prettier/oxfmt)
187+
fmt: fmt-server fmt-web fmt-infra ## Format server (gofmt+goimports), web (oxfmt), and infra (oxfmt)
181188

182189
fmt-server: ## Format Go code (writes)
183190
@echo "$(CYAN)Formatting Go code...$(RESET)"
184191
cd $(SERVER_DIR) && $(GOFMT) -s -w . && $(GOIMPORTS_CMD) -w .
185192

186-
fmt-check: fmt-check-server fmt-check-web ## Verify formatting on Go + web (matches GitHub `web-checks` / `server-checks` jobs)
193+
fmt-check: fmt-check-server fmt-check-web fmt-check-infra ## Verify formatting on Go + web + infra (matches GitHub `web-checks` / `server-checks` / `infra-checks` jobs)
187194

188195
fmt-check-server: ## Verify Go is gofmt-clean (CI-friendly; exit 1 on drift)
189196
@cd $(SERVER_DIR) && out=$$($(GOFMT) -s -l .); \
@@ -199,11 +206,20 @@ fmt-web: ## Format web client (writes)
199206
fmt-check-web: ## Verify web client is oxfmt-clean
200207
cd $(WEB_DIR) && npm run format:check
201208

202-
typecheck: typecheck-web ## Type-check (web only; Go type-checks in `vet`/`build`)
209+
fmt-infra: ## Format CDK infra (writes)
210+
cd $(INFRA_DIR) && npm run format
211+
212+
fmt-check-infra: ## Verify CDK infra is oxfmt-clean
213+
cd $(INFRA_DIR) && npm run format:check
214+
215+
typecheck: typecheck-web typecheck-infra ## Type-check (web + infra; Go type-checks in `vet`/`build`)
203216

204217
typecheck-web: ## Type-check TypeScript (tsc --noEmit)
205218
cd $(WEB_DIR) && npm run typecheck
206219

220+
typecheck-infra: ## Type-check CDK infra TypeScript (tsc --noEmit)
221+
cd $(INFRA_DIR) && npm run typecheck
222+
207223
##@ CI
208224

209225
ci: vet fmt-check lint typecheck test test-server-e2e build ## Full CI gate (vet, fmt-check, lint, typecheck, test, e2e, build). Mirrors `.github/workflows/ci.yml`; needs Docker for e2e.

infra/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,13 +121,17 @@ CDK_DEFAULT_REGION=us-east-1
121121

122122
# Route 53 hosted zone ID for the domain
123123
ROUTE53_ZONE_ID=<your-zone-id>
124+
125+
# Pre-issued ACM cert (wildcard + per-env subdomain SANs) imported by NetworkingStack
126+
CERTIFICATE_ARN=<your-cert-arn>
124127
```
125128

126129
| Variable | Required | Description |
127130
|----------|----------|-------------|
128131
| `CDK_DEFAULT_ACCOUNT` | Yes | AWS account ID where stacks are deployed |
129132
| `CDK_DEFAULT_REGION` | Yes | AWS region (e.g., `us-east-1`) |
130133
| `ROUTE53_ZONE_ID` | Yes | Route 53 hosted zone ID for the domain |
134+
| `CERTIFICATE_ARN` | Yes | Pre-issued ACM cert ARN to import in NetworkingStack |
131135

132136
These are loaded automatically via `dotenv` when CDK runs. Do **not** put AWS credentials (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`) in this file — use `aws configure` or AWS SSO instead.
133137

infra/lib/config.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export interface AppConfig {
2727
readonly projectName: string;
2828
readonly domainName: string;
2929
readonly route53ZoneId: string;
30-
readonly allSubdomains: string[];
30+
readonly certificateArn: string;
3131
readonly staging: StagingEnvConfig;
3232
readonly production: ProductionEnvConfig;
3333
}
@@ -36,7 +36,10 @@ export const APP_CONFIG: AppConfig = {
3636
projectName: "mattermost-test-system-io",
3737
domainName: "test.mattermost.com",
3838
route53ZoneId: process.env.ROUTE53_ZONE_ID ?? "",
39-
allSubdomains: ["staging-test-io", "test-io"],
39+
// Imported by ARN rather than created in-stack so a CDK version bump
40+
// can't change a property default and force a PENDING_VALIDATION
41+
// replacement.
42+
certificateArn: process.env.CERTIFICATE_ARN ?? "",
4043

4144
staging: {
4245
subdomain: "staging-test-io",

infra/lib/constructs/networking.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,16 @@ export interface NetworkingProps {
1212
readonly projectName: string;
1313
readonly vpcCidr?: string;
1414
readonly domainName: string;
15-
readonly subdomainNames: string[];
1615
readonly route53ZoneId: string;
16+
readonly certificateArn: string;
1717
readonly appPort?: number;
1818
}
1919

2020
export class Networking extends Construct {
2121
public readonly vpc: ec2.Vpc;
2222
public readonly alb: elbv2.ApplicationLoadBalancer;
2323
public readonly httpsListener: elbv2.ApplicationListener;
24-
public readonly certificate: acm.Certificate;
24+
public readonly certificate: acm.ICertificate;
2525
public readonly appSecurityGroup: ec2.SecurityGroup;
2626
public readonly hostedZone: route53.IHostedZone;
2727

@@ -70,13 +70,14 @@ export class Networking extends Construct {
7070
zoneName: props.domainName,
7171
});
7272

73-
// ACM certificate with DNS validation via Route 53
74-
const subjectAlternativeNames = props.subdomainNames.map((sub) => `${sub}.${props.domainName}`);
75-
this.certificate = new acm.Certificate(this, "Certificate", {
76-
domainName: `*.${props.domainName}`,
77-
subjectAlternativeNames,
78-
validation: acm.CertificateValidation.fromDns(this.hostedZone),
79-
});
73+
// Imported, not created in-stack: a CDK version bump can otherwise
74+
// drift a property default and trigger a Certificate replacement
75+
// that stalls in PENDING_VALIDATION.
76+
this.certificate = acm.Certificate.fromCertificateArn(
77+
this,
78+
"Certificate",
79+
props.certificateArn,
80+
);
8081

8182
// ALB security group — allow HTTPS/HTTP from anywhere (IPv4 + IPv6)
8283
const albSecurityGroup = new ec2.SecurityGroup(this, "AlbSecurityGroup", {

infra/lib/stacks/networking_stack.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export class NetworkingStack extends cdk.Stack {
1616
public readonly vpc: ec2.Vpc;
1717
public readonly alb: elbv2.ApplicationLoadBalancer;
1818
public readonly httpsListener: elbv2.ApplicationListener;
19-
public readonly certificate: acm.Certificate;
19+
public readonly certificate: acm.ICertificate;
2020
public readonly appSecurityGroup: ec2.SecurityGroup;
2121
public readonly hostedZone: route53.IHostedZone;
2222

@@ -25,12 +25,23 @@ export class NetworkingStack extends cdk.Stack {
2525

2626
const { config } = props;
2727

28+
if (!config.route53ZoneId) {
29+
throw new Error(
30+
"ROUTE53_ZONE_ID env var is required (Route 53 hosted zone — see infra/lib/config.ts)",
31+
);
32+
}
33+
if (!config.certificateArn) {
34+
throw new Error(
35+
"CERTIFICATE_ARN env var is required (pinned ACM cert ARN — see infra/lib/config.ts)",
36+
);
37+
}
38+
2839
const networking = new Networking(this, "Networking", {
2940
environment: "shared",
3041
projectName: config.projectName,
3142
domainName: config.domainName,
32-
subdomainNames: config.allSubdomains,
3343
route53ZoneId: config.route53ZoneId,
44+
certificateArn: config.certificateArn,
3445
});
3546

3647
this.vpc = networking.vpc;

infra/test/app_stack.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { APP_CONFIG, AppConfig } from "../lib/config";
1010
const testConfig: AppConfig = {
1111
...APP_CONFIG,
1212
route53ZoneId: "Z1234567890",
13+
certificateArn:
14+
"arn:aws:acm:us-east-1:123456789012:certificate/00000000-0000-0000-0000-000000000000",
1315
};
1416

1517
const testEnv = { account: "123456789012", region: "us-east-1" };
@@ -80,9 +82,12 @@ describe("Infrastructure", () => {
8082
templates.networking.resourceCountIs("AWS::ElasticLoadBalancingV2::LoadBalancer", 1);
8183
});
8284

83-
test("creates ACM certificate with DNS validation", () => {
84-
templates.networking.hasResourceProperties("AWS::CertificateManager::Certificate", {
85-
ValidationMethod: "DNS",
85+
test("imports ACM certificate by ARN (no in-stack cert resource)", () => {
86+
// Re-introducing an in-stack Certificate would re-expose us to
87+
// CDK-version-drift forced replacements that stall in PENDING_VALIDATION.
88+
templates.networking.resourceCountIs("AWS::CertificateManager::Certificate", 0);
89+
templates.networking.hasResourceProperties("AWS::ElasticLoadBalancingV2::Listener", {
90+
Certificates: [{ CertificateArn: testConfig.certificateArn }],
8691
});
8792
});
8893
});

0 commit comments

Comments
 (0)