Skip to content

Instantly share code, notes, and snippets.

@lawrencejones
Created June 4, 2020 11:07
Show Gist options
  • Save lawrencejones/35ae5c842deb0d34305a1fb3e48fd2ba to your computer and use it in GitHub Desktop.
Save lawrencejones/35ae5c842deb0d34305a1fb3e48fd2ba to your computer and use it in GitHub Desktop.
Draft gc-app
local app = import 'app.libsonnet';
app {
_config+:: {
release: 'release-bot',
namespace: 'connect-team',
environment: 'production',
app: 'release-bot',
image: 'eu.gcr.io/gc-containers/gocardless/release-bot',
tag: 'TODO',
google_project: 'gc-prd-effc',
env: [
{ name: 'PGDATABASE', value: 'release_bot' },
],
},
resources: {
server_default:
$.server.new(containers=[
$.container.new(name='app', command=['./serve']) +
$.container.withHealthCheck(path='/health_check', port=8080) +
$.container.withPorts(
$.container.newPort(8080),
),
]),
},
}
local k = import '../../../kubernetes/vendor/k8s.libsonnet';
{
local cfg = self._config,
_config:: {
// Common labels that should be applied to all resources. Find more
// information at:
//
// https://github.com/gocardless/anu/wiki/Kubernetes%3a-Labels
release: error 'required',
namespace: error 'required',
app: error 'required',
// Unique environment name, used to identify this service via the service
// registry
environment: error 'required',
// Each app has a primary image, usually linked to the git repo in which it
// lives. All resources are grouped into a release (set by the above label)
// and each type of workload will default to a single container pod, running
// code from this image.
//
// The image tag is provided as an external variable by our deployment
// system, or override in the service file if it should be static.
image: error 'required',
tag: std.extVar('tag') || 'unknown',
// This is the GCP project in which this release lives, so the home of the
// GKE cluster that hosts it. That differs from the GCP project that hosts
// the infrastructure, or what the config connector is configured to manage.
//
// It might make more sense for the service account to live in the GCP
// project that homes its resources, but workload identity enforces the
// restriction that you can bind only to accounts in the GKE cluster
// project.
google_project: error 'required',
google_service_account: '%s-%s@%s.iam.gserviceaccount.com' % [
cfg.namespace,
cfg.release,
cfg.google_project,
],
// This field is a list of environment variables which will be populated
// into the default container configuration created by the default workload
// constructors.
//
// Use kubectl to explain the structure of this list:
// $ kubectl explain deployment.spec.template.spec.containers.env
env: [
// {
// name: 'PGDATABASE',
// value: 'database',
// },
// {
// name: 'PGPASSWORD',
// valueFrom: {
// secretKeyRef: {
// key: 'release-secret',
// name: 'key',
// },
// },
// },
],
},
// Collect common RBAC resource types together, to avoid different resources
// using different versions.
local clusterRole = k.rbac.v1.clusterRole,
local clusterRoleBinding = k.rbac.v1.clusterRoleBinding,
local role = k.rbac.v1.role,
local roleBinding = k.rbac.v1.roleBinding,
local ruleSpec = role.rulesType,
local serviceAccount = k.core.v1.serviceAccount,
local subject = roleBinding.subjectsType,
helpers:: {
// Generate ServiceAccount, Role, RoleBinding triple, all sharing the same
// name.
namespacedRBAC(name, rules):: {
service_account:
serviceAccount.new(name) +
serviceAccount.mixin.metadata.withNamespace(cfg.namespace),
role:
role.new() +
role.mixin.metadata.withName(name) +
role.mixin.metadata.withNamespace(cfg.namespace) +
role.withRules(rules),
role_binding:
roleBinding.new() +
roleBinding.mixin.metadata.withName(name) +
roleBinding.mixin.metadata.withNamespace(cfg.namespace) +
roleBinding.mixin.roleRef.withApiGroup('rbac.authorization.k8s.io') +
roleBinding.mixin.roleRef.withKind('Role') +
roleBinding.mixin.roleRef.withName(name) +
roleBinding.withSubjects([
subject.new() +
subject.withKind('ServiceAccount') +
subject.withName(name) +
subject.withNamespace($._config.namespace),
]),
},
},
// Each release has a default Kubernetes service account, bound to a Google
// service account (if required). The service account is bound to a role that
// has no permissions.
//
// Extending the release Kubernetes service account with additional
// permissions is possible by mixing-in additional rules:
//
// app {
// identity+: {
// role+: role.withRuleMixin([
// -- additional rules go here
// ]),
// },
// }
//
identity: $.helpers.namespacedRBAC(cfg.release, []) {
service_account+:
serviceAccount.mixin.metadata.withAnnotationsMixin({
'iam.gke.io/gcp-service-account': cfg.google_service_account,
}),
},
// As with RBAC, collect common workload resources together, ensuring we use
// consistent versions.
local deployment = k.apps.v1.deployment,
local podTemplate = deployment.mixin.spec.templateType,
local container = deployment.mixin.spec.template.spec.containersType,
local containerPort = container.portsType,
container:: container {
new(name, command, env=cfg.env, ports=[], probe=null)::
container.new(name) +
container.withImage('%s:%s' % [cfg.image, cfg.tag]) +
container.withEnv(env) +
container.withCommand(command) +
container.withPorts(ports),
// Create a container port suitable for entering into
// $ kubectl explain pod.spec.containers.ports
newPort(containerPort, name='metrics', protocol='TCP')::
{
name: name,
protocol: protocol,
containerPort: containerPort,
},
// Configure a health check on the liveness and readiness probes. Readiness
// is set to be the same as the liveness check, unless specific overrides
// are configured.
withHealthCheck(path='/health_check', port=8080, readyPath=path, readyPort=port)::
container.mixin.livenessProbe.httpGet.withPath(path) +
container.mixin.livenessProbe.httpGet.withPort(port) +
container.mixin.readinessProbe.httpGet.withPath(path) +
container.mixin.readinessProbe.httpGet.withPort(port),
},
// Used to model the default workload type. Concrete types should extend this
// with a custom new() method.
workload:: {
new(name, containers, replicas, role)::
local labels = {
app: cfg.app,
release: cfg.release,
version: cfg.tag,
role: role,
component: name,
};
super.new() +
self.mixin.metadata.withName('%s-%s' % [role, name]) +
self.mixin.metadata.withNamespace(cfg.namespace) +
// Apply the same labels to both the kind (deployment, statefulset,
// whatever) as we use for the selector. Labels used in a selector can't
// change, so put metadata in annotations if necessary.
self.mixin.metadata.withLabels(labels) +
self.mixin.spec.selector.withMatchLabels(labels) +
self.mixin.spec.template.metadata.withLabels(labels) +
// Each workload needs replicas- default to 1, so we run everything by
// default
self.mixin.spec.withReplicas(replicas) +
// TODO: Disable auto-mount by default, in favour of a mixin that uses
// projected volume mounts to provide a Kubernetes service account. This
// would help opt-in containers that want Kubernetes connectivity, while
// keeping sidecars/untrusted code from exercising those paths.
self.mixin.spec.template.spec.withServiceAccountName(cfg.release) +
self.mixin.spec.template.spec.withAutomountServiceAccountToken(true) +
self.mixin.spec.template.spec.withContainers(containers),
},
// Workloads that provide a network service of some type, whether an HTTP API
// or some type of RPC server. Previously called backends.
server:: deployment + self.workload {
new(name='default', ports=(error 'servers must have ports'), containers=[], replicas=1)::
super.new(name=name, containers=containers, replicas=replicas, role='server'),
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment