Today I decided to perform a simple scripting task in Golang. One of
the challenging aspects of coding with AWS is testing code locally. In
AWS we usually use instance
credentials
for anything that requires permissions to AWS resources. This happens
automagically via the AWS sdk when you run code within AWS. The
trouble is that when you try to test the same code locally, it doesn’t
have access to these instance roles - so you need to handle that.
There are a few different ways to provide credentials to the
sdk.
At my job we historically have used environment variables. We even
wrote a script that sourced credentials from a yaml file and
exported the correct env variables. The workflow went something like
this:
eval $(script to export env variables)
Run your program, which wold now find the needed env variables.
This works great, until you open a new shell and forget to update the
env. Or you update ~/.profile to have the right env but you need to
switch to a different AWS account and still need to update your env
when you switch shells. Or you need to test something ran by a
supervisor like daemontools and
it’s env is different unless you take the time to modify it. You get
the idea. Environment variables are lovely, but it winds up being a
pain. You test for 10 minutes only to realize you’ve been hitting the
wrong AWS account because the env got dropped somewhere.
My personal solution to this problem goes something like this:
Stop using a custom yaml file to store AWS credentials.
Stop using env variables for testing outside of AWS.
Start using the AWS sdk defined credentials
file
(and it’s format).
With this approach, so long as you have the default account defined
for a particular user - everything just works. This is very close to
how it works when ran within AWS (via instance roles) and this is what
I wanted to mimic.
Automate the AWS sdk credentials file
The AWS sdk supports profiles, which is grand - but I was trying to
mimic instance credentials. I needed the code to work using whatever
it was granted by default. This meant I needed to easily be able to
change the default credentials (versus adding conditional logic that
would only be used during development/testing). More specifically I
wanted to describe each AWS account I used as a profile in the AWS
credentials file, but toggle the default one via automation. I also
had the practical reality of a custom credentials yaml file… so I
wanted to be able to reference that as a fall-back if a profile wasn’t
defined. Typically this is a task I would have quickly written up in
Python… but I wanted to do it in
Golang
:)
This struck me as a nice coding exercise in a language I wanted to
learn. It was clearly not a challenging task, and involved a few
common things that I’d need to learn anyway:
The end result was as you’d expect. The code was pretty simple, about
100 lines. It wasn’t hard to write, and does the job nicely. But I did
have a few different areas where I got stuck.
I’ve historically always written most every command line argument with
both short and long forms. For example -e and --environment which
would hold a string representing the desired environment. With Golang
I was surprised to learn that the flag
library only supports a single type of expression that happens to
mimic
google-gflags.
This means you can use -e or -env but not both. Interestingly I
have observed that you can use - or -- with whatever argument you
specify.
Considering how common (and old) ini files are I honestly expected
Golang to handle this via stdlib. Turns out this is not the case, but
go-ini worked nicely. The only part
that took a little time was learning how to actually mutate a value in
the object representation of the ini file. I read the documentation a
few times, searched for phrases like set and write but didn’t find
anything on how to actually change a value. I then noticed a link to
the api documentation and the
same phrase search found what I was looking for:
SetValue.
This was where things became challenging (for me). I was working with
yaml that looked something like this:
qa1:
key: foo
secret: bar
staging:
key: foo
secret: bar
When I started looking for information on how to consume this type of
structure, I started by searching for
examples.
All of the documentation I found talked about creating a
struct. I really liked this
idea because I would have a type
to describe the data I was consuming. This felt clean, organized,
consistent with my desire to learn Golang in the first place. I saw
lots of examples along the lines of this:
type T struct {
A string
B struct {
RenamedC int `yaml:"c"`
D []int `yaml:",flow"`
}
}
I tried a bunch of permutations but always wound up with an empty data
structure after calling
Unmarshal. I noticed
the yaml type hints in many of the documents I read, but chasing that
turned out to be a waste of time. Fundamentally I was looking to
consume data that was a simple nested map. A map of maps, who’s keys
and values were all strings. I was struggling on how to do this as all
of the examples minimally defined the top level structure, even if
below that it was just a map[string]string. Because I clearly am out
of my league, I decided to take the path of least resistance. So I
created a struct to describe each pair:
type Environment struct {
key string
secret string
}
Then I actually enumerated the known aws keypairs I was dealing with,
so something like this:
Nothing. At this point I wondered if I was actually even testing what
I was testing. Maybe I was consuming the wrong yaml file and passing
it to the ini library, or maybe I was consuming the right file… but
was passing the wrong data type to the library. I was really
scratching my head. Eventually I learned that the first letter of the
field defined in the struct had to be capitalized. For example if the
top level key in the yaml file was named env1 then the struct needed
to be Capitalized. So the following made things spring to life:
I was consuming the yaml successfully, whoop! At this point I was
pretty much finished with the program. But there was this tiny little
part left. If I could not find the named profile in the credentials
file, I needed to see if I could find it from the custom yaml file.
For example if I was trying to set env2 as the default account but
there was no such profile in the credentials file, I needed to fetch
that information from the yaml file, via it’s capitalized field name.
This seemed great, until I realized I was trying to find a
struct field name based on
a lower cased string representation of it. This was fine though, I’m
no stranger to reflection
(getattr
happens to be one of my favorite programming questions, to see if
someone who doesn’t now the language can learn something new).
This sent me down a trail of how to use
reflect. I was able to reflectively
gain access to the top level keys via something similar to:
r := reflect.ValueOf(myStruct)
value := reflect.Indirect(r).FieldByName("Env1")
But then I got lost on .Elem(), .Type() and things like
.Type().String(). I was chasing things I very much didn’t
understand.
It was a this point that I thought back to the original
problem - I was dealing with yaml who’s top level keys were not firmly
defined, and I wanted to reference them via a
map. If I could just do that,
I’d be set. So I tried again to define a type, that described a map of
maps. I tried things like:
type CustomCredentials struct {
map[string]map[string]string
}
and
var CustomCredentials = map[string]map[string]string
Finally I stumbled upon a site somewhere (I honestly forget where)
that mentioned the syntax to do exactly this:
type LegacyAwsCreds map[string]map[string]string
Boom, I was done. Writing the program wasn’t hard, but easily 75%
of the time was dominated by trying to learn how to use the custom
yaml file that contained the keys.
I also learned a quick lesson in scoping. Initially I was trying
things like this:
value, err := getCredentialsFromProfile(key)
if err != nil {
value = getCredentialsFromYamlFile(key)
}
key.SetValue(value)
I quickly realized this is not allowed, and updated my program to
calculate the value and return it to something else who’s job it was
to use the value. Something closer to this:
value, err = getCredentialsFromProfile()
if err != nil {
return getCredentialsFromYamlFile()
}
return value
This worked nicely, and caused me to have better separation of
concerns anyway. Felt nice.
The program worked out great. It’s small, easy to read, fast, and
something I can easily share with co-workers. I especially like the
part where they can download the binary and not have to satisfy
dependencies like PyYAML :)
I was using the program recently and noticed that I had AWS keys at
the root of the credentials file, versus in a env specific section. I
figured the issue was some sort of defect in my code. I spent a good
chunk of time trying to understand why or how I could create a
section, write a key to that section, and then persist the
configuration to a file and have they keys - but not in the section
specified.
Here’s a snippet that I expected to produce a section with a single
key:
cfg.Section("section").NewKey("key", "value")
Here the Section() function fetches a named section and creates it
if it doesn’t already exist. It only returns the section object, and
thus it’s legal to immediately use that to add a NewKey(). I stared
at this for some time and was simply unable to spot a defect in my
usage of the library. It was only by accident that I noticed a certain
behavior. If the ini file opened by ini.load() did not exist on
disk, the resulting SaveTo() call would persist the keys without a
section. However if the ini file did exist (even if it was an empty
file) the section and it’s keys were persisted. So basically I was
able to fix the defect by
touching the ini file
prior to running the program. Ultimately I fixed the program by having
the program itself create an empty file if missing, prior to calling
ini.load().
I’ll file an issue upstream
just in case this is actually a defect. Here’s an example program to
illustrate the behavior:
$ go run test.go
&{true {{0 0} 0 0 0 0} [{/tmp/missing-file.ini}] map[] [] <nil>} open /tmp/missing-file.ini: no such file or directory
$ cat /tmp/missing-file.ini
key = value
Because the write ultimately succeeded, running the same program again
has this result:
$ go run test.go
&{true {{0 0} 0 0 0 0} [{/tmp/missing-file.ini}] map[DEFAULT:0xc820018190] [DEFAULT] <nil>} <nil>
$ cat /tmp/missing-file.ini
key = value
[section]
key = value