Skip to content

Commit 9df3678

Browse files
author
Steven Gettys
committed
Azure MSI examples for go and docker
0 parents  commit 9df3678

9 files changed

+356
-0
lines changed

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.terraform
2+
terraform.tfstate*

Dockerfile

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM golang:1.15.0 as builder
2+
WORKDIR /go/src/msigolang/
3+
COPY . /go/src/msigolang/
4+
RUN go test ./... -v
5+
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o run .
6+
7+
FROM alpine:3.8
8+
RUN apk --update add ca-certificates
9+
WORKDIR /root/
10+
COPY --from=builder /go/src/msigolang/run .
11+
CMD ["./run"]

README.md

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
# Azure GO MSI
2+
This is an example repo for debugging the differences between using MSI for getting an Azure KV secret in a Go based Docker Azure Web App and a Go based Azure Container Instance. Web Apps do not seem to properly support MSI for this use case while the Container Instance does.
3+
4+
This repo has a simple Go app that will lookup a secret from a KV instance using a user managed identity. The Terraform will create a resource group, key vault, managed identity and access policies, and a test instace for both an Azure Web App and Azure Container Instance to see if they can retreive the secret using that managed identity.
5+
6+
# Docker Image
7+
To test this first build and publish the `Dockerfile`:
8+
```
9+
docker build --rm -t <dockerhub repo>/<container name>:<version> .
10+
docker push <dockerhub repo>/<container name>:<version>
11+
```
12+
13+
Make sure that this is a publicly accessible container image so that it can be pulled into the Web App and Container Instance
14+
15+
# Terraform
16+
The Terraform requires 2 inputs, a name for the deployment, and the docker image that is in dockerhub. To run the Terraform do the following:
17+
```
18+
terraform init
19+
terraform play --var "name=<name to use>" --var "docker_image=<docker image in dockerhub>"
20+
terraform apply --var "name=<name to use>" --var "docker_image=<docker image in dockerhub>"
21+
```
22+
23+
Once this has been created then check the Web App logs and the Container Instance logs to see if they were able to retreive the secret. This can be done using the portal by navigating to the resource group created by the Terraform.
24+
25+
# Go Application
26+
The Go app can use 2 possible methods for using the MSI to get the KV secret.
27+
28+
Method 1:
29+
```
30+
msiKeyConfig := &auth.MSIConfig{
31+
Resource: strings.TrimSuffix(azure.PublicCloud.KeyVaultEndpoint, "/"),
32+
ClientID: clientID,
33+
}
34+
35+
authorizer, err := msiKeyConfig.Authorizer()
36+
```
37+
38+
Method 2:
39+
```
40+
authorizer, err := auth.NewAuthorizerFromEnvironment()
41+
```
42+
43+
The authorizer method can be changed in the `NewKeyVaultClient` method by changing which authorizer method to use, either `getMSIAuthorizer` or `getAuthorizerFromEnv`.
44+
45+
When testing a new auth method the docker container must be rebuilt and pushed with a new version and then that version must be deployed to Azure using the Terraform and the "docker_image" variable.
46+
47+
# New Version
48+
```
49+
# Make code change
50+
# Rebuild Docker app
51+
docker build --rm -t <image name>:<new version> .
52+
docker push <image name>:<new version>
53+
terraform apply --var "docker_image=<image name>:<new version>" --var "name=<name>" --auto-approve
54+
```

go.mod

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
module github.com/sgetty/azure-go-msi
2+
3+
go 1.13
4+
5+
require (
6+
github.com/Azure/azure-sdk-for-go v47.1.0+incompatible
7+
github.com/Azure/go-autorest/autorest v0.11.10
8+
github.com/Azure/go-autorest/autorest/azure/auth v0.5.3
9+
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
10+
github.com/Azure/go-autorest/autorest/validation v0.3.0 // indirect
11+
)

go.sum

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
github.com/Azure/azure-sdk-for-go v47.1.0+incompatible h1:D6MsWmsxF+pEjN/yZDyKXoUrsamdBdTlPedIgBlvVx4=
2+
github.com/Azure/azure-sdk-for-go v47.1.0+incompatible/go.mod h1:9XXNKU+eRnpl9moKnB4QOLf1HestfXbmab5FXxiDBjc=
3+
github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs=
4+
github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24=
5+
github.com/Azure/go-autorest/autorest v0.11.9/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw=
6+
github.com/Azure/go-autorest/autorest v0.11.10 h1:j5sGbX7uj1ieYYkQ3Mpvewd4DCsEQ+ZeJpqnSM9pjnM=
7+
github.com/Azure/go-autorest/autorest v0.11.10/go.mod h1:eipySxLmqSyC5s5k1CLupqet0PSENBEDP93LQ9a8QYw=
8+
github.com/Azure/go-autorest/autorest/adal v0.9.5 h1:Y3bBUV4rTuxenJJs41HU3qmqsb+auo+a3Lz+PlJPpL0=
9+
github.com/Azure/go-autorest/autorest/adal v0.9.5/go.mod h1:B7KF7jKIeC9Mct5spmyCB/A8CG/sEz1vwIRGv/bbw7A=
10+
github.com/Azure/go-autorest/autorest/azure/auth v0.5.3 h1:lZifaPRAk1bqg5vGqreL6F8uLC5V0fDpY8nFvc3boFc=
11+
github.com/Azure/go-autorest/autorest/azure/auth v0.5.3/go.mod h1:4bJZhUhcq8LB20TruwHbAQsmUs2Xh+QR7utuJpLXX3A=
12+
github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 h1:dMOmEJfkLKW/7JsokJqkyoYSgmR08hi9KrhjZb+JALY=
13+
github.com/Azure/go-autorest/autorest/azure/cli v0.4.2/go.mod h1:7qkJkT+j6b+hIpzMOwPChJhTqS8VbsqqgULzMNRugoM=
14+
github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw=
15+
github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74=
16+
github.com/Azure/go-autorest/autorest/mocks v0.4.1 h1:K0laFcLE6VLTOwNgSxaGbUcLPuGXlNkbVvq4cW4nIHk=
17+
github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k=
18+
github.com/Azure/go-autorest/autorest/to v0.4.0 h1:oXVqrxakqqV1UZdSazDOPOLvOIz+XA683u8EctwboHk=
19+
github.com/Azure/go-autorest/autorest/to v0.4.0/go.mod h1:fE8iZBn7LQR7zH/9XU2NcPR4o9jEImooCeWJcYV/zLE=
20+
github.com/Azure/go-autorest/autorest/validation v0.3.0 h1:3I9AAI63HfcLtphd9g39ruUwRI+Ca+z/f36KHPFRUss=
21+
github.com/Azure/go-autorest/autorest/validation v0.3.0/go.mod h1:yhLgjC0Wda5DYXl6JAsWyUe4KVNffhoDhG0zVzUMo3E=
22+
github.com/Azure/go-autorest/logger v0.2.0 h1:e4RVHVZKC5p6UANLJHkM4OfR1UKZPj8Wt8Pcx+3oqrE=
23+
github.com/Azure/go-autorest/logger v0.2.0/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8=
24+
github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo=
25+
github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU=
26+
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
27+
github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
28+
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
29+
github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk=
30+
github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k=
31+
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
32+
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
33+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
34+
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
35+
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
36+
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
37+
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE=
38+
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
39+
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
40+
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
41+
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
42+
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

main.go

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"os"
8+
"strings"
9+
10+
"github.com/Azure/azure-sdk-for-go/services/keyvault/2016-10-01/keyvault"
11+
"github.com/Azure/go-autorest/autorest"
12+
"github.com/Azure/go-autorest/autorest/azure"
13+
"github.com/Azure/go-autorest/autorest/azure/auth"
14+
)
15+
16+
func main() {
17+
vaultName, ok := os.LookupEnv("KEYVAULT_VAULT_NAME")
18+
if !ok {
19+
log.Fatal("KEYVAULT_VAULT_NAME must be set.")
20+
}
21+
22+
secretName, ok := os.LookupEnv("KEYVAULT_SECRET_NAME")
23+
if !ok {
24+
log.Fatal("KEYVAULT_SECRET_NAME must be set.")
25+
}
26+
27+
clientID := os.Getenv("MSI_USER_ASSIGNED_CLIENTID")
28+
29+
keyClient, err := NewKeyVaultClient(vaultName, clientID)
30+
if err != nil {
31+
log.Fatal(err)
32+
}
33+
34+
secret, err := keyClient.GetSecret(secretName)
35+
if err != nil {
36+
log.Fatal(err)
37+
}
38+
39+
log.Printf("Retrieved secret '%s' from keyvault using MSI", secret)
40+
}
41+
42+
// KeyVault holds the information for a keyvault instance
43+
type KeyVault struct {
44+
client *keyvault.BaseClient
45+
vaultURL string
46+
}
47+
48+
// NewKeyVaultClient creates a new keyvault client
49+
func NewKeyVaultClient(vaultName, clientID string) (*KeyVault, error) {
50+
// Change this to change auth method. Add new auth methods to change how
51+
// auth is setup
52+
authorizer, err := getMSIAuthorizer(clientID)
53+
// authorizer, err := getAuthorizerFromEnv()
54+
if err != nil {
55+
return nil, err
56+
}
57+
58+
keyClient := keyvault.New()
59+
keyClient.Authorizer = authorizer
60+
61+
k := &KeyVault{
62+
vaultURL: fmt.Sprintf("https://%s.%s", vaultName, azure.PublicCloud.KeyVaultDNSSuffix),
63+
client: &keyClient,
64+
}
65+
66+
return k, nil
67+
}
68+
69+
func getMSIAuthorizer(clientID string) (autorest.Authorizer, error) {
70+
msiKeyConfig := &auth.MSIConfig{
71+
Resource: strings.TrimSuffix(azure.PublicCloud.KeyVaultEndpoint, "/"),
72+
ClientID: clientID,
73+
}
74+
75+
return msiKeyConfig.Authorizer()
76+
}
77+
78+
func getAuthorizerFromEnv() (autorest.Authorizer, error) {
79+
return auth.NewAuthorizerFromEnvironment()
80+
}
81+
82+
// GetSecret retrieves a secret from keyvault
83+
func (k *KeyVault) GetSecret(keyName string) (string, error) {
84+
ctx := context.Background()
85+
86+
keyBundle, err := k.client.GetSecret(ctx, k.vaultURL, keyName, "")
87+
if err != nil {
88+
return "", err
89+
}
90+
91+
return *keyBundle.Value, nil
92+
}

main.tf

+131
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# Setup the resource group, key vault, and container registry
2+
provider "azurerm" {
3+
features {}
4+
}
5+
resource "random_string" "default" {
6+
length = 4
7+
special = false
8+
}
9+
data "azurerm_client_config" "current" {}
10+
11+
locals {
12+
name_prefix = "${var.name}-${random_string.default.result}"
13+
}
14+
resource "azurerm_resource_group" "default" {
15+
name = "${local.name_prefix}-rsg"
16+
location = var.location
17+
}
18+
resource "azurerm_key_vault" "default" {
19+
name = "${local.name_prefix}-kv"
20+
location = var.location
21+
tenant_id = data.azurerm_client_config.current.tenant_id
22+
sku_name = "standard"
23+
resource_group_name = azurerm_resource_group.default.name
24+
}
25+
resource "azurerm_key_vault_access_policy" "secret_set" {
26+
key_vault_id = azurerm_key_vault.default.id
27+
28+
tenant_id = data.azurerm_client_config.current.tenant_id
29+
object_id = data.azurerm_client_config.current.object_id
30+
31+
secret_permissions = [
32+
"get",
33+
"set",
34+
"delete",
35+
]
36+
}
37+
resource "azurerm_key_vault_secret" "example" {
38+
depends_on = [azurerm_key_vault_access_policy.secret_set]
39+
key_vault_id = azurerm_key_vault.default.id
40+
name = "test-secret"
41+
value = "A Secret"
42+
}
43+
resource "azurerm_user_assigned_identity" "default" {
44+
resource_group_name = azurerm_resource_group.default.name
45+
location = azurerm_resource_group.default.location
46+
47+
name = "${var.name}-msi"
48+
}
49+
50+
resource "azurerm_key_vault_access_policy" "secret_get" {
51+
key_vault_id = azurerm_key_vault.default.id
52+
53+
tenant_id = data.azurerm_client_config.current.tenant_id
54+
object_id = azurerm_user_assigned_identity.default.principal_id
55+
56+
secret_permissions = [
57+
"get",
58+
]
59+
}
60+
61+
resource "azurerm_app_service_plan" "default" {
62+
name = "${local.name_prefix}-svc-plan"
63+
resource_group_name = azurerm_resource_group.default.name
64+
location = var.location
65+
kind = "Linux"
66+
reserved = true
67+
68+
sku {
69+
tier = "Standard"
70+
size = "S1"
71+
}
72+
}
73+
74+
resource "azurerm_app_service" "default" {
75+
name = "${local.name_prefix}-app"
76+
resource_group_name = azurerm_resource_group.default.name
77+
location = var.location
78+
app_service_plan_id = azurerm_app_service_plan.default.id
79+
https_only = true
80+
81+
identity {
82+
type = "UserAssigned"
83+
identity_ids = [azurerm_user_assigned_identity.default.id]
84+
}
85+
86+
site_config {
87+
always_on = true
88+
linux_fx_version = "DOCKER|${var.docker_image}"
89+
}
90+
91+
app_settings = {
92+
"WEBSITES_ENABLE_APP_SERVICE_STORAGE" = "false"
93+
"WEBSITES_PORT" = "8080"
94+
"KEYVAULT_VAULT_NAME" = "${azurerm_key_vault.default.name}"
95+
"KEYVAULT_SECRET_NAME" = "${azurerm_key_vault_secret.example.name}"
96+
"MSI_USER_ASSIGNED_IDENTITY_CLIENTID" = "${azurerm_user_assigned_identity.default.client_id}"
97+
}
98+
auth_settings {
99+
enabled = false
100+
}
101+
}
102+
103+
resource "azurerm_container_group" "example" {
104+
name = "${local.name_prefix}-container"
105+
location = azurerm_resource_group.default.location
106+
resource_group_name = azurerm_resource_group.default.name
107+
ip_address_type = "public"
108+
dns_name_label = "aci-label"
109+
os_type = "Linux"
110+
identity {
111+
type = "UserAssigned"
112+
identity_ids = [azurerm_user_assigned_identity.default.id]
113+
}
114+
115+
container {
116+
name = "hello-world"
117+
image = var.docker_image
118+
cpu = "0.5"
119+
memory = "1.5"
120+
ports {
121+
port = 443
122+
protocol = "TCP"
123+
}
124+
environment_variables = {
125+
"KEYVAULT_VAULT_NAME" = "${azurerm_key_vault.default.name}"
126+
"KEYVAULT_SECRET_NAME" = "${azurerm_key_vault_secret.example.name}"
127+
"MSI_USER_ASSIGNED_IDENTITY_CLIENTID" = "${azurerm_user_assigned_identity.default.client_id}"
128+
129+
}
130+
}
131+
}

outputs.tf

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
output "resource_group_name" {
2+
value = azurerm_resource_group.default.name
3+
}

variables.tf

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
variable "name" {
2+
description = "Name to use"
3+
}
4+
variable "location" {
5+
description = "Region to deploy to"
6+
default = "westus2"
7+
}
8+
variable "docker_image" {
9+
description = "Docker image to deploy"
10+
}

0 commit comments

Comments
 (0)