This is an ERv2 (External Resources v2) module that provisions and manages AWS MSK Connect resources (custom plugins, worker configurations, and Kafka connectors) via app-interface. It is owned by the AppSRE team at Red Hat.
This project is modeled directly after app-sre/er-aws-msk, which provisions AWS MSK clusters. The local copy lives at ~/code/er-aws-msk. Always consult that repo when unsure about patterns, conventions, or framework usage. The two projects share identical architecture -- only the AWS resources and data models differ.
This module is tightly coupled to app-interface (~/code/app-interface), which is the configuration-as-code system where users declare the resources this module provisions. Understanding the app-interface side is essential for end-to-end context.
Each ERv2 module has a module definition file in data/external-resources/modules/. For the existing MSK cluster module, this is data/external-resources/modules/msk-1.yml:
$schema: /external-resources/module-1.yml
provision_provider: aws
provider: msk
module_type: terraform
reconcile_drift_interval_minutes: 1440
reconcile_timeout_minutes: 120
outputs_secret_sync: true
channels:
- name: stable
image: quay.io/redhat-services-prod/app-sre-tenant/er-aws-msk-main/er-aws-msk-main
version: 0.7.0-36This module will need an equivalent file (e.g., data/external-resources/modules/msk-connect-1.yml) with provider: msk-connect pointing to the er-aws-msk-connect container image.
Resources are declared in namespace YAML files under data/services/<team>/<service>/namespaces/. The pattern is:
managedExternalResources: true
externalResources:
- provider: aws
provisioner:
$ref: /aws/<account>/account.yml
resources:
- provider: msk # this would be "msk-connect" for connectors
identifier: <unique-id>
managed_by_erv2: true
defaults: /terraform/resources/path/to/defaults.yml
output_resource_name: <k8s-secret-name>The actual resource configuration lives in defaults files under resources/terraform/resources/. For MSK clusters, these use $schema: /aws/msk-defaults-1.yml and contain all the Terraform-level config (kafka version, instance type, subnets, etc.). This module will need a new schema (e.g., /aws/msk-connect-defaults-1.yml) with fields matching the MskConnectData Pydantic model.
The primary MSK clusters are declared in the Insights/ConsoleDot strimzi service:
- Stage:
data/services/insights/strimzi/namespaces/stage-platform-mq-stage.yml(identifier:consoledot-stage) - Prod:
data/services/insights/strimzi/namespaces/platform-mq-prod.yml(identifier:consoledot-prod) - Perf:
data/services/insights/strimzi/namespaces/perf-platform-mq-perf.yml(identifier:consoledot-perf)
Defaults files live under resources/terraform/resources/insights/{production,stage,perf}/.
Assisted Installer also has MSK clusters in data/services/assisted-installer/namespaces/.
All existing Kafka Connect deployments in app-interface are self-managed Strimzi/AMQ Streams KafkaConnect running on OpenShift -- not AWS MSK Connect. Key deployments:
- Kessel Kafka Connect:
data/services/insights/kessel/-- runs Debezium connectors (e.g.,kessel-inventory-api-connector,hbi-host-migration-connector). Connects to MSK using SASL/SCRAM. - xjoin-kafka-connect: Strimzi-based, in the platform-mq namespaces.
This module would be the first use of AWS-managed MSK Connect in the organization. There are zero existing references to mskconnect or msk-connect in app-interface.
When this module is ready for deployment, the following must be added to app-interface:
- Module definition:
data/external-resources/modules/msk-connect-1.yml(registers the container image and provider) - Defaults schema: A new JSON schema for MSK Connect defaults (e.g.,
/aws/msk-connect-defaults-1.yml) - Defaults files: Per-environment configuration under
resources/terraform/resources/ - Namespace declarations: Add
provider: msk-connectresources to the appropriate namespace files - Provider exclusion: Add
msk-connecttoterraformResourcesProviderExclusionsindata/app-interface/app-interface-settings.yml(ensures ERv2-only handling)
The ERv2 module pattern works as follows:
- A user declares a connector in app-interface (a configuration-as-code system).
qontract-cligenerates aninput.jsonfile describing the desired state.- This module's Python code (
er_aws_msk_connect/__main__.py) parses that input using Pydantic models and generates Terraform backend config and variable files. - The
er-base-terraformbase image runsterraform planand then the post-plan hook (hooks/post_plan.py) validates the plan against live AWS APIs. - If validation passes,
terraform applycreates the resources. - Terraform outputs are fed back as secrets to the target Kubernetes namespace.
All ERv2 modules depend on the external-resources-io library. It provides:
parse_model()/read_input_from_file()-- input parsingAppInterfaceProvision-- base Pydantic model for provisioning metadatacreate_backend_tf_file()/create_tf_vars_json()-- Terraform config generationTerraformJsonPlanParser/Action/ResourceChange-- plan parsing for hooksConfig-- configuration management- CLI tool for generating
variables.tffrom Pydantic models (external-resources-io tf generate-variables-tf)
The Pydantic models in app_interface_input.py define the input schema. The MskConnectData model fields are serialized to JSON and passed as Terraform variables. The field names and nesting in the Pydantic models must exactly match the Terraform variable structure. The variables.tf file is auto-generated from the Pydantic models via make generate-variables-tf -- never hand-edit it.
When create_tf_vars_json() is called with exclude_none=False, fields with None values are serialized as null in JSON. Terraform code must handle these nulls correctly (e.g., using count conditionals or try()).
| Resource | Terraform Type | Notes |
|---|---|---|
| Custom Plugin | aws_mskconnect_custom_plugin |
Single plugin per connector. Users combine JARs into one ZIP. |
| Worker Configuration | aws_mskconnect_worker_configuration |
Optional. |
| CloudWatch Log Group | aws_cloudwatch_log_group |
Conditional on log delivery config. |
| Connector | aws_mskconnect_connector |
The core resource. IAM auth and TLS encryption are hardcoded. |
- One connector per module invocation. Do not add support for multiple connectors in a single Terraform state.
- One custom plugin per connector. Users must combine multiple JARs into a single ZIP. Do not add support for multiple plugins.
- S3 bucket and plugin files are pre-existing. This module does not create S3 buckets or upload plugin artifacts.
- IAM authentication is hardcoded. It is the only valid auth type for MSK Connect. Do not make this configurable.
- TLS encryption is hardcoded. Required for IAM auth.
- The
service_execution_role_arnis required and pre-existing. This module does not create IAM roles.
Most MSK Connect resources force replacement on any change:
aws_mskconnect_custom_plugin: ALL arguments force replacement. Any change destroys and recreates.aws_mskconnect_worker_configuration: ALL arguments force replacement.aws_mskconnect_connector: Onlycapacityandconnector_configurationcan be updated in-place. Everything else (name, kafka_cluster, plugins, worker_config, service_execution_role, encryption, auth) forces replacement.
Both the custom plugin and worker configuration resources use create_before_destroy lifecycle rules and identifier-prefixed naming to handle replacements safely.
# Set up local dev environment
make dev
# Run linting, type checking, and tests locally
uv run ruff check
uv run ruff format
uv run mypy
uv run pytest -vv
# Build and test in container (uses podman)
make test
# Build prod image
make build
# Regenerate variables.tf from Pydantic models (after changing app_interface_input.py)
make generate-variables-tf
# Update terraform provider lock after bumping versions.tf
make providers-lock- All AWS API calls in hooks are mocked via
unittest.mock.patch - Shared fixtures live in
tests/conftest.pywith araw_input_datadict and parsedai_input - Tests run inside the container during CI via
make in_container_test - Static analysis: ruff (ALL rules enabled), mypy (strict with Pydantic plugin), terraform fmt
The Dockerfile is a multi-stage build based on er-base-terraform-main:
base-- sets up paths and venvbuilder-- installs deps, syncs terraform providers, copies sourceprod-- minimal runtimetest-- adds dev deps, runsmake in_container_test
Uses uv for Python dependency management. The uv.lock file must stay in sync with pyproject.toml (enforced by uv lock --locked in the build).
Tekton pipelines in .tekton/ via Konflux (Pipelines as Code):
- PRs build the
teststage (runs all tests) - Pushes to
mainbuild theprodstage and push to quay.io
- Never hand-edit
terraform/variables.tf-- it is auto-generated from Pydantic models. - Never hand-edit
uv.lock-- useuv lockoruv addto modify dependencies. - Pydantic model field names must match Terraform variable nesting exactly -- mismatches cause silent failures where Terraform receives null for fields it expects to be set.
- Use
podmaninstead ofdocker-- this is a Fedora/RHEL environment. - The
capacityblock uses two mutually exclusive dynamic blocks in Terraform (autoscaling vs provisioned_capacity). The mutual exclusion is enforced at the Pydantic layer via a@model_validator.