Skip to content

Commit

Permalink
KeyVault integration (#14)
Browse files Browse the repository at this point in the history
This PR enables spec.remoteSecrets and populates spec.app.properties.configuration.secrets based on it's configuration. It will copy the value from the specified secret in Azure KeyVault to the Container App secrets, making sure you don't have to specify any secrets in git.
  • Loading branch information
simongottschlag authored Sep 1, 2022
1 parent afd5db2 commit 6abbc60
Show file tree
Hide file tree
Showing 23 changed files with 785 additions and 46 deletions.
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,12 @@ terraform-up:
terraform apply -auto-approve -var-file="../../.tmp/lab.tfvars"

run:
# AZURE_TENANT_ID=$${TENANT_ID} AZURE_CLIENT_ID=$${CLIENT_ID} AZURE_CLIENT_SECRET=$${CLIENT_SECRET} \
go run ./src \
--resource-group-name $${RG_NAME} \
--subscription-id $${SUB_ID} \
--managed-environment-id $${ME_ID} \
--key-vault-name $${KV_NAME} \
--location westeurope \
--reconcile-interval "10s" \
--checkout-path "/tmp/foo" \
Expand All @@ -59,6 +61,7 @@ docker-run: docker-build
--resource-group-name $${RG_NAME} \
--subscription-id $${SUB_ID} \
--managed-environment-id $${ME_ID} \
--key-vault-name $${KV_NAME} \
--location westeurope \
--reconcile-interval "10s" \
--checkout-path "/tmp/foo" \
Expand Down
45 changes: 43 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Platform is used for what we call "platform services", in this case the virtual

Tenant is used only to synchronize the Container Apps manifests. The Container Apps that are created by `azcagit` will reside here.

The manifests are in the same format as Kubernetes manifests ([Kubernetes Resource Model aka KRM](https://cloud.google.com/blog/topics/developers-practitioners/build-platform-krm-part-2-how-kubernetes-resource-model-works)), but with a hard coupling to the [Azure Container Apps specification](https://docs.microsoft.com/en-us/azure/templates/microsoft.app/containerapps?pivots=deployment-language-arm-template).
The manifests are in the same format as Kubernetes manifests ([Kubernetes Resource Model aka KRM](https://cloud.google.com/blog/topics/developers-practitioners/build-platform-krm-part-2-how-kubernetes-resource-model-works)), but with a hard coupling to the [Azure Container Apps specification](https://docs.microsoft.com/en-us/azure/templates/microsoft.app/containerapps?pivots=deployment-language-arm-template) for `spec.app`.

An example manifest:

Expand All @@ -29,6 +29,9 @@ apiVersion: aca.xenit.io/v1alpha1
metadata:
name: foobar
spec:
remoteSecrets:
- appSecretName: connection-string
remoteSecretName: mssql-connection-string
app:
properties:
configuration:
Expand All @@ -40,22 +43,59 @@ spec:
resources:
cpu: 0.25
memory: .5Gi
env:
- name: CONNECTION_STRING
secretRef: connection-string
- name: MEANING_WITH_LIFE
value: "forty two"
scale:
minReplicas: 1
maxReplicas: 1
```
YAML-files can contain one or more documents (with `---` as a document separator). As of right now, all files in the git repository path (configured with `--git-path` when launching `azcagit`) needs to pass validation for any deletion to occur (deletion will be disabled if any manifests contains validation errors).

## Frequently Asked Questions

> What happens if a manifest can't be parsed?

Reconciliation will stop an no changes (add/delete/update) will be made until the parse error is fixed.

> What happens if a secret in the KeyVault is defined in a manifest but doesn't exist?

Reconciliation will stop an no changes (add/delete/update) will be made until the secret is added to the KeyVault or it's removed from the manifest.

> What happens if a secret is changed in the KeyVault?

The Container App will be updated at the next reconcile.

> What happens if I add the tag `aca.xenit.io=true` to a Container App in the tenant resource group, without the app being defined in a manifest?

It will be removed at the next reconcile.

> What happens if I remove the tag `aca.xenit.io=true` from a Container App in the tenant resource group, while still having a manifest for it?

It won't be reconciled anymore. Depending on the order, a few apps before will still be reconciled but none after.

> What happens if I add the tag `aca.xenit.io=true` to a Container App in the tenant resource group, while it's also defined in a manifest?

It will be updated based on the manifest.

> What properties, as of now, can't be used even though they are defined in the Azure Container Apps specification?

- `spec.app.properties.managedEnvironmentID`: it's defined by azcagit
- `spec.app.location`: it's defined by azcagit

## Things TODO in the future

- [ ] Append secrets to Container Apps from KeyVault
- [x] Append secrets to Container Apps from KeyVault
- [x] ~~Better error handling of validation failures (should deletion be stopped?)~~ _stop reconciliation on any parsing error_
- [ ] Push git commit status (like [Flux notification-controller](https://fluxcd.io/docs/components/notification/provider/#git-commit-status))
- [ ] Health checks
- [ ] Metrics
- [x] Manually trigger reconcile
- [x] Enforce Location for app
- [ ] Add Container Registry credentials by default

## Usage

Expand Down Expand Up @@ -103,6 +143,7 @@ The following parameters can be used
RG_NAME=resource_group_name
SUB_ID=azure_subscription_id
ME_ID=azure_container_apps_managed_environment_id
KV_NAME=kvcontainerapps
GIT_URL_AND_CREDS=git_url_with_optional_credentials
```

Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.19
require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.1.2
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.7.1
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers v1.0.0
github.com/alexflint/go-arg v1.4.3
Expand All @@ -21,6 +22,7 @@ require (

require (
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.5.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v0.6.0 // indirect
github.com/Masterminds/semver/v3 v3.1.1 // indirect
github.com/Microsoft/go-winio v0.5.2 // indirect
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0 h1:QkAcEIAKbNL4KoFr4Sath
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.1.0/go.mod h1:bhXu1AjYL+wutSL/kpSq6s7733q2Rb0yuot9Zgfqa/0=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0 h1:jp0dGvZ7ZK0mgqnTSClMxa5xuRL7NZgHameVYF6BurY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.0.0/go.mod h1:eWRD7oawr1Mu1sLCawqVc0CUiF43ia3qQMxLscsKQ9w=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.7.1 h1:X7FHRMKr0u5YiPnD6L/nqG64XBOcK0IYavhAHBQEmms=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azsecrets v0.7.1/go.mod h1:WcC2Tk6JyRlqjn2byvinNnZzgdXmZ1tOiIOWNh1u0uA=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.5.0 h1:9cn6ICCGiWFNA/slKnrkf+ENyvaCRKHtuoGtnLIAgao=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.5.0/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA=
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.1.0 h1:ebO2jmZyctLSMBTvjsxZv/Ml3rGsvnJHUImVWotBl7I=
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.1.0/go.mod h1:LH9XQnMr2ZYxQdVdCrzLO9mxeDyrDFa6wbSI3x5zCZk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers v1.0.0 h1:zIQzosd251uW2j2+MIbMDeyqkISOFV88XYE7pvkWIZM=
Expand Down
12 changes: 12 additions & 0 deletions src/azure/azure.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package azure

import "github.com/Azure/azure-sdk-for-go/sdk/azidentity"

func NewAzureCredential() (*azidentity.DefaultAzureCredential, error) {
cred, err := azidentity.NewDefaultAzureCredential(nil)
if err != nil {
return nil, err
}

return cred, nil
}
16 changes: 8 additions & 8 deletions src/cache/cache.go → src/cache/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,19 @@ import (
"github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers"
)

type CacheEntry struct {
type AppCacheEntry struct {
modified time.Time
sourceAppHash string
}

type Cache map[string]CacheEntry
type AppCache map[string]AppCacheEntry

func NewCache() *Cache {
c := make(Cache)
func NewAppCache() *AppCache {
c := make(AppCache)
return &c
}

func (c *Cache) Set(name string, remoteApp, sourceApp *armappcontainers.ContainerApp) {
func (c *AppCache) Set(name string, remoteApp, sourceApp *armappcontainers.ContainerApp) {
if remoteApp == nil {
return
}
Expand All @@ -42,16 +42,16 @@ func (c *Cache) Set(name string, remoteApp, sourceApp *armappcontainers.Containe
}
hash := fmt.Sprintf("%x", md5.Sum(b))

(*c)[name] = CacheEntry{
(*c)[name] = AppCacheEntry{
modified: *timestamp,
sourceAppHash: hash,
}
}

func (c *Cache) NeedsUpdate(name string, remoteApp, sourceApp *armappcontainers.ContainerApp) (bool, string) {
func (c *AppCache) NeedsUpdate(name string, remoteApp, sourceApp *armappcontainers.ContainerApp) (bool, string) {
entry, ok := (*c)[name]
if !ok {
return true, "not in cache"
return true, "not in AppCache"
}

if remoteApp == nil {
Expand Down
41 changes: 41 additions & 0 deletions src/cache/secret.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cache

import "time"

type SecretCacheEntry struct {
name string
value string
modified time.Time
}
type SecretCache map[string]SecretCacheEntry

func NewSecretCache() *SecretCache {
c := make(SecretCache)
return &c
}

func (c *SecretCache) Set(name string, value string, modified time.Time) {
(*c)[name] = SecretCacheEntry{
name,
value,
modified,
}
}

func (c *SecretCache) Get(name string) (string, bool) {
entry, ok := (*c)[name]
return entry.value, ok
}

func (c *SecretCache) NeedsUpdate(name string, modified time.Time) bool {
entry, ok := (*c)[name]
if !ok {
return true
}

if modified != entry.modified {
return true
}

return false
}
1 change: 1 addition & 0 deletions src/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ type Config struct {
ResourceGroupName string `json:"resource_group_name" arg:"-g,--resource-group-name,env:RESOURCE_GROUP_NAME,required" help:"Azure Resource Group Name"`
SubscriptionID string `json:"subscription_id" arg:"-s,--subscription-id,env:AZURE_SUBSCRIPTION_ID,required" help:"Azure Subscription ID"`
ManagedEnvironmentID string `json:"managed_environment_id" arg:"-m,--managed-environment-id,env:MANAGED_ENVIRONMENT_ID,required" help:"Azure Container Apps Managed Environment ID"`
KeyVaultName string `json:"key_vault_name" arg:"-k,--key-vault-name,env:KEY_VAULT_NAME,required" help:"Azure KeyVault name to extract secrets from"`
Location string `json:"location" arg:"-l,--location,env:LOCATION,required" help:"Azure Region (location)"`
ReconcileInterval string `json:"reconcile_interval" arg:"-i,--reconcile-interval,env:RECONCILE_INTERVAL" default:"5m" help:"The interval between reconciles"`
CheckoutPath string `json:"checkout_path" arg:"-c,--checkout-path,env:CHECKOUT_PATH,required" help:"The local path where the git repository should be checked out"`
Expand Down
3 changes: 3 additions & 0 deletions src/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ func TestNewConfig(t *testing.T) {
"bar",
"--managed-environment-id",
"baz",
"--key-vault-name",
"ze-keyvault",
"--location",
"westeurope",
"--checkout-path",
Expand All @@ -28,6 +30,7 @@ func TestNewConfig(t *testing.T) {
ResourceGroupName: "foo",
SubscriptionID: "bar",
ManagedEnvironmentID: "baz",
KeyVaultName: "ze-keyvault",
Location: "westeurope",
ReconcileInterval: "5m",
CheckoutPath: "/tmp/foo",
Expand Down
19 changes: 16 additions & 3 deletions src/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ import (
"time"

"github.com/go-logr/logr"
"github.com/xenitab/azcagit/src/azure"
"github.com/xenitab/azcagit/src/cache"
"github.com/xenitab/azcagit/src/config"
"github.com/xenitab/azcagit/src/logger"
"github.com/xenitab/azcagit/src/reconcile"
"github.com/xenitab/azcagit/src/remote"
"github.com/xenitab/azcagit/src/secret"
"github.com/xenitab/azcagit/src/source"
"github.com/xenitab/azcagit/src/trigger"
"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -54,14 +56,25 @@ func run(ctx context.Context, cfg config.Config) error {
return fmt.Errorf("unable to get source: %w", err)
}

remoteClient, err := remote.NewAzureRemote(cfg)
cred, err := azure.NewAzureCredential()
if err != nil {
return err
}

cache := cache.NewCache()
remoteClient, err := remote.NewAzureRemote(cfg, cred)
if err != nil {
return err
}

secretClient, err := secret.NewKeyVaultSecret(cfg, cred)
if err != nil {
return err
}

appCache := cache.NewAppCache()
secretCache := cache.NewSecretCache()

reconciler, err := reconcile.NewReconciler(sourceClient, remoteClient, cache)
reconciler, err := reconcile.NewReconciler(sourceClient, remoteClient, secretClient, appCache, secretCache)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit 6abbc60

Please sign in to comment.