Dogfooding Blambda 3: CLIify this!

When last we left the epic tale of me trying to use a thing that I made myself and then being outraged at how poorly made the thing was, I had just gotten dependencies stuffed in a lambda layer and proved that it worked by deploying a lambda that listed some files from an S3 bucket. Hurrah!

Of course, I ended that post by complaining about the incredibly bad UX of the thing I had built, and a vague promise to make it less incredibly bad (never let good be the enemy of less bad!). Since last we spoke, I did just that. Check it out!

So I create a directory called my-lambda, then add a bb.edn like this:

{:deps {net.jmglov/blambda
        #_"You use the newest SHA here:"
        {:git/url "https://github.com/jmglov/blambda.git"
         :git/sha "adcef76ed415bbe65bb348fcbf4ba271efcab6e4"}}
 :tasks
 {:requires ([blambda.cli :as blambda])
  blambda {:doc "Controls Blambda runtime and layers"
           :task (blambda/dispatch)}}}

This is enough to let me use Blambda:

$ bb blambda
Usage: bb blambda <subcommand> <options>

All subcommands support the options:

  --target-dir <dir> target Build output directory
  --work-dir   <dir> .work  Working directory

Subcommands:

build-runtime-layer: Builds Blambda custom runtime layer
  --bb-version <version> 0.9.161 Babashka version
  --bb-arch    <arch>            Architecture to target (use amd64 if you don't care)

build-deps-layer: Builds dependencies layer from bb.edn or deps.edn
  --deps-path <path> Path to bb.edn or deps.edn containing lambda deps

deploy-runtime-layer: Deploys Blambda custom runtime layer
  --aws-region         <region> eu-west-1 AWS region
  --runtime-layer-name <name>   blambda   Name of custom runtime layer in AWS
  --bb-arch            <arch>   amd64     Architecture to target

deploy-deps-layer: Deploys dependencies layer
  --aws-region      <region> eu-west-1 AWS region
  --deps-layer-name <name>             Name of dependencies layer in AWS

clean: Removes work and target folders

I can create a Blambda runtime:

$ bb blambda build-runtime-layer --help
Usage: bb blambda build-runtime-layer <options>

Options:
  --target-dir <dir>     target  Build output directory
  --work-dir   <dir>     .work   Working directory
  --bb-version <version> 0.9.161 Babashka version
  --bb-arch    <arch>            Architecture to target

$ bb blambda build-runtime-layer --bb-arch arm64
Downloading https://github.com/babashka/babashka/releases/download/v0.9.161/babashka-0.9.161-linux-aarch64-static.tar.gz
Decompressing .work/babashka-0.9.161-linux-aarch64-static.tar.gz to .work
Adding file bootstrap
Adding file bootstrap.clj
Compressing custom runtime layer: ~/my-lambda/target/bb.zip

And deploy it:

$ bb blambda deploy-runtime-layer --bb-arch arm64
Publishing layer version for layer blambda
Published layer arn:aws:lambda:eu-west-1:289341159200:layer:blambda:1

Now, let's say I want my lambda to do S3 stuff using awyeah-api. I'll create a src directory and drop a bb.edn in there:

{:paths ["."]
 :deps {com.cognitect.aws/endpoints {:mvn/version "1.1.12.206"}
        com.cognitect.aws/s3 {:mvn/version "822.2.1109.0"}
        com.grzm/awyeah-api {:git/url "https://github.com/grzm/awyeah-api"
                             :git/sha "0fa7dd51f801dba615e317651efda8c597465af6"}
        org.babashka/spec.alpha {:git/url "https://github.com/babashka/spec.alpha"
                                 :git/sha "433b0778e2c32f4bb5d0b48e5a33520bee28b906"}}}

I can now use Blambda to create a lambda layer containing all my dependencies:

$ bb blambda build-deps-layer
Missing required arguments: --deps-path

Usage: bb blambda build-deps-layer <options>

Options:
  --target-dir <dir>  target Build output directory
  --work-dir   <dir>  .work  Working directory
  --deps-path  <path>        Path to bb.edn or deps.edn containing lambda deps

Oops! Looks like I forgot the --deps-path argument. Let's try that again:

$ bb blambda build-deps-layer --deps-path src/bb.edn 
Cloning: https://github.com/grzm/awyeah-api
Downloading: com/cognitect/aws/endpoints/1.1.12.206/endpoints-1.1.12.206.pom from central
Downloading: org/clojure/clojure/1.11.1/clojure-1.11.1.pom from central
Downloading: com/cognitect/aws/s3/822.2.1109.0/s3-822.2.1109.0.pom from central
Checking out: https://github.com/grzm/awyeah-api at 0fa7dd51f801dba615e317651efda8c597465af6
Cloning: https://github.com/babashka/spec.alpha
Checking out: https://github.com/babashka/spec.alpha at 433b0778e2c32f4bb5d0b48e5a33520bee28b906
Downloading: org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.pom from central
Downloading: org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.pom from central
Downloading: org/clojure/pom.contrib/1.1.0/pom.contrib-1.1.0.pom from central
Downloading: com/cognitect/aws/endpoints/1.1.12.206/endpoints-1.1.12.206.jar from central
Downloading: com/cognitect/aws/s3/822.2.1109.0/s3-822.2.1109.0.jar from central
Downloading: org/clojure/spec.alpha/0.3.218/spec.alpha-0.3.218.jar from central
Downloading: org/clojure/clojure/1.11.1/clojure-1.11.1.jar from central
Downloading: org/clojure/core.specs.alpha/0.2.62/core.specs.alpha-0.2.62.jar from central
Classpath before transforming: src:~/my-lambda/.work/m2-repo/com/cognitect/aws/endpoints/1.1.12.206/endpoints-1.1.12.206.jar:...

Classpath after transforming: src:/opt/m2-repo/com/cognitect/aws/endpoints/1.1.12.206/endpoints-1.1.12.206.jar:...

Compressing custom runtime layer: ~/my-lambda/target/deps.zip

What that wall of text (sorry about that!) is saying is that Blambda is downloading all of the dependencies my lambda has declared in bb.edn and zipping them up into a layer, which I can deploy like this:

$ bb blambda deploy-deps-layer --deps-layer-name my-lambda-deps
Publishing layer version for layer my-lambda-deps
Published layer arn:aws:lambda:eu-west-1:289341159200:layer:my-lambda-deps:1

It is a little annoying to have to remember those arguments every time, so let's see what we can do about that. If I go back to my top-level bb.edn, I can specify some defaults for my lambda:

{:deps {net.jmglov/blambda
        #_"You use the newest SHA here:"
        {:git/url "https://github.com/jmglov/blambda.git"
         :git/sha "adcef76ed415bbe65bb348fcbf4ba271efcab6e4"}}
 :tasks
 {:requires ([blambda.cli :as blambda])
  :init (def opts {:deps-path "src/bb.edn"
                   :deps-layer-name "my-lambda-deps"
                   :bb-arch "arm64"})
  blambda {:doc "Controls Blambda runtime and layers"
           :task (blambda/dispatch opts)}}}

By adding the opts there, I've saved myself the trouble of typing --bb-arch, --deps-path, and --deps-layer-name:

$ bb blambda build-deps-layer
Classpath before transforming: src:~/my-lambda/.work/m2-repo/com/cognitect/aws/endpoints/1.1.12.206/endpoints-1.1.12.206.jar:...

Classpath after transforming: src:/opt/m2-repo/com/cognitect/aws/endpoints/1.1.12.206/endpoints-1.1.12.206.jar:...

Compressing custom runtime layer: ~/my-lambda/target/deps.zip

I can now create a lambda in the AWS console that uses Blambda and my deps layer.

AWS Lambda console showing create function dialog

Now I need to add the custom runtime layer:

AWS Lambda console showing add layer dialog with Blambda layer selected

And the deps layer:

AWS Lambda console showing add layer dialog with deps layer selected

Looks nice! Now how about some Clojure code that does something exciting?

We can delete the files that AWS has put there and replace them with a my_lambda.clj that looks like this:

(ns my-lambda
  (:require [cheshire.core :as json]))

(defn handler [event _context]
  (let [body {:msg "Lambda handler invoked"
              :data {:event event}}]
    (prn body)
    {:status 200
     :body (json/generate-string body)}))

The Cheshire JSON library is included free of charge by Babashka, so we can play with JSON despite not mentioning it in our src/bb.edn. Amazing!

We also need to change Runtime settings > Handler to my-lambda/handler, then click the Deploy button to get our lambda out there in the world!

AWS Lambda console showing the code described above

After deploying, we excitedly click the Test button, only to be told that we need to configure a test event! 😭 No matter, we'll just go with the hello-world template and hope for the best.

AWS Lambda console showing a simple JSON test event

Now that we have a test event configured, let's click Test again... and celebrate a job well done! 🎉

AWS Lambda console showing a successful lambda execution

I had actually intended to explain the CLI stuff in this post, but this is already a bit long and my dog is starting to give me meaningful looks, so I'd better end this thrilling instalment of Dogfooding Blambda here and take him out for a walk.

Discuss this post here.

Published: 2022-08-10

Tagged: clojure aws blambda babashka lambda

Dogfooding Blambda : I heard you liked layers

After a brief detour to port my blog to quickblog and enjoy some sun in Málaga, I'm back to eating yummy dogfood by trying to use Blambda to parse my HTTP access logs: jmglov/s3-log-parser.

I didn't realise it until I started writing this post, but it has actually been a month since I last used Blambda; I'm sure you all remember the wacky hijinks that ensued when I tried to make Blambda work with Babashka pods. Well, more wacky hijinks ensued when I came back to the project and realised that the released version of the babasha-aws pod was compiled for AMD64, and my lambda is of course ARM64 because I want to be cool. Instead of doing what a reasonable person would do and switch to an AMD64 lambda, I decided instead to use awyeah-api, which is a port of Cognitect's aws-api to Babashka. You know, because it's cool to not have to rely on an external binary and bbencode and all of that stuff. 😉

Of course, Blambda didn't actually support dependencies outside of pods, so the first step to switching to awyeah-api was implementing regular deps in Blambda. How hard could it be, right?

Just like I did for pods, I wanted to keep all deps in a layer so that it would be fast to deploy new versions of the lambda itself, and deps typically don't change very often. This should be pretty straightforward: just get Babashka to download my deps and zip them up into a layer.

I created a bb.edn with all of the awyeah-api stuff:

{:paths ["."]
 :deps {com.cognitect.aws/endpoints {:mvn/version "1.1.12.206"}
        com.cognitect.aws/s3 {:mvn/version "822.2.1109.0"}
        com.grzm/awyeah-api {:git/url "https://github.com/grzm/awyeah-api"
                             :git/sha "0fa7dd51f801dba615e317651efda8c597465af6"}
        org.babashka/spec.alpha {:git/url "https://github.com/babashka/spec.alpha"
                                 :git/sha "433b0778e2c32f4bb5d0b48e5a33520bee28b906"}}}

Now, running bb tasks in that directory downloaded all of the dependencies into my local Maven cache, ~/.m2/repository, so I just need to grab them and stuff them into a zip file. Except there seem to be about a million Java libraries in that directory, and I definitely don't want more than I actually need, since I want to keep my layer zipfile as small as possible. So what I would like to do is create a new directory and tell Babashka to use that as the Maven cache, so I know that everything downloaded there belongs to my lambda. But how to tell Babashka where I want my deps?

Searching the web wasn't much help, mainly because I couldn't find the right magic phrase, so I turned to Clojurians Slack for help. borkdude himself stopped by and told me that Babashka uses the Clojure CLI to resolve dependencies, and then Alex Miller (who must have a Slack alert for any mention of -Sdeps) told me about the :mvn/local-repo key, which does exactly what I want. Excellent!

And then I ran into the next issue: in addition to Maven dependencies, I also have Git dependencies, and these are handled differently. Going back to the Deps and CLI Reference, I found that one can set the GITLIBS environment variable to tell the CLI where to cache Git deps.

So now I knew how to get all of my deps right where I wanted them, so it was time to automate things with Babashka. I created a new build-deps task in Blamba's bb.edn. The first step was to consume the lambda's bb.edn and add the :mvn/local-repo key to it:

build-deps {:docs "Builds a layer for dependencies"
            :requires ([clojure.edn :as edn])
            :task (let [{:keys [deps-path work-dir]} (th/parse-args)]
                    (when-not deps-path
                      (th/error "Mising required argument: --deps-path"))
                    (fs/create-dirs work-dir)
                    (let [m2-dir "m2-repo"
                          deps (->> deps-path slurp edn/read-string :deps)]
                      (spit (fs/file work-dir "deps.edn")
                            {:deps deps
                             :mvn/local-repo (str m2-dir)})))}

This will create a .work/deps.edn file that contains all of the lambda's dependency, plus the magic :mvn/local-repo key to tell tools.deps to use .work/m2-repo for my Maven deps.

Now I need to run clojure somehow to download the deps into the right place. Chatting to borkdude revealed that there's a babashka.deps/clojure function that invokes the Clojure CLI. Awesome, so I can use that! I just need to not forget to set the GITLIBS env var to .work/gitlibs to make sure the Git deps end up in the right place.

build-deps {:docs "Builds a layer for dependencies"
            :requires ([clojure.edn :as edn]
                       [babashka.deps :refer [clojure]])
            :task (let [{:keys [deps-path work-dir]} (th/parse-args)]
                    ;; ...
                    (let [gitlibs-dir "gitlibs"
                          m2-dir "m2-repo"
                          deps (->> deps-path slurp edn/read-string :deps)]
                      ;; ...
                      (clojure ["-Spath"]
                               {:dir work-dir
                                :env (assoc (into {} (System/getenv))
                                            "GITLIBS" (str gitlibs-dir))})))}

And now that I have all the deps, I just need to zip them up:

build-deps {:docs "Builds a layer for dependencies"
            :requires ([clojure.java.shell :refer [sh]]
                       [clojure.edn :as edn]
                       [babashka.deps :refer [clojure]])
            :task (let [{:keys [deps-path target-dir work-dir]} (th/parse-args)
                        deps-zipfile (th/deps-zipfile target-dir)]
                    ;; ...
                    (fs/create-dirs target-dir work-dir)
                    (let [gitlibs-dir "gitlibs"
                          m2-dir "m2-repo"
                          deps (->> deps-path slurp edn/read-string :deps)]
                      ;; ...
                      (println "Compressing custom runtime layer:" deps-zipfile)
                      (let [{:keys [exit err]}
                            (sh "zip" "-r" deps-zipfile
                                (fs/file-name gitlibs-dir)
                                (fs/file-name m2-dir)
                                :dir work-dir)]
                        (when (not= 0 exit)
                          (println "Error:" err)))))}

This gives me a target/deps.zip that I can use as a layer. Lambda layers unzip to /opt, so I'll need to update the Blambda runtime to add GITLIBS=/opt/gitlibs to my Babashka invocation and update my lambda's bb.edn to include :mvn/local-repo "/opt/m2-repo", and then of course add the deps layer to my lambda function.

Once all this was done, I took a deep breath and pressed the Test button in the AWS Lambda console. And of course got an error. 😭

Exception in thread "main" java.lang.Exception: Couldn't find 'java'.
Please set JAVA_HOME.
  at borkdude.deps$_main.invokeStatic(deps.clj:436)
  at borkdude.deps$_main.doInvoke(deps.clj:425)
  at clojure.lang.RestFn.applyTo(RestFn.java:137)
  at clojure.core$apply.invokeStatic(core.clj:667)
  at babashka.impl.deps$add_deps$fn__26630$fn__26631.invoke(deps.clj:92)
  at babashka.impl.deps$add_deps$fn__26630.invoke(deps.clj:92)
  at babashka.impl.deps$add_deps.invokeStatic(deps.clj:92)
  at babashka.main$exec.invokeStatic(main.clj:789)
  at babashka.main$main.invokeStatic(main.clj:1052)
  at babashka.main$main.doInvoke(main.clj:1027)
  at clojure.lang.RestFn.applyTo(RestFn.java:137)
  at clojure.core$apply.invokeStatic(core.clj:667)
  at babashka.main$_main.invokeStatic(main.clj:1085)
  at babashka.main$_main.doInvoke(main.clj:1077)
  at clojure.lang.RestFn.applyTo(RestFn.java:137)
  at babashka.main.main(Unknown Source)

Yikes! I guess this makes sense, though. Babashka is using the Clojure CLI to resolve dependencies, and the Clojure CLI needs a JVM. Unfortunately, this won't work for me, because the lambda container doesn't have Java installed.

At this point, having learned a lot about what's going on under the hood in Babashka, I decided to cheat and see how Holy Lambda solved this problem for the Babashka backend. Here's what HL's bootstrap script looks like:

#!/bin/sh

set -e

export BABASHKA_DISABLE_SIGNAL_HANDLERS="true"

export XDG_CACHE_HOME=/opt
export XDG_CONFIG_HOME=/opt
export XDG_DATA_HOME=/opt
export HOME=/var/task
export GITLIBS=/opt
export CLOJURE_TOOLS_DIR=/opt
export CLJ_CACHE=/opt
export CLJ_CONFIG=/opt

export BABASHKA_CLASSPATH="/opt/.m2:var/task/src:/var/task/.m2:/var/task:/var/task/src/clj:/var/task/src/cljc:src/cljc:src/clj:/var/task/resources"
export BABASHKA_PRELOADS='(load-file "/opt/hacks.clj")'

if [ -z "$HL_ENTRYPOINT" ]; then
  echo "Environment variable \"HL_ENTRYPOINT\" is not set. See https://fierycod.github.io/holy-lambda/#/babashka-backend-tutorial"
fi;

/opt/bb -Duser.home=/var/task -m "$HL_ENTRYPOINT"

Aha! HL sets the Babashka classpath explicitly so that Babashka won't need to do any resolution, and furthermore, that exciting looking hacks.clj actually disables dependency resolution altogether:

(require '[babashka.deps])

(alter-var-root
 #'babashka.deps/add-deps
 (fn [f]
   (fn [m]
     (println "[holy-lambda] Dependencies should not be added via add-deps. Move your dependencies to a layer!")
     (System/exit 1))))

Sneaky but awesome! I'll have to copy this later, but for now, let me see if I can just make things work.

I'll take inspiration from Holy Lambda and set the classpath explicitly, so the first thing to do is figure out what the classpath should be. Luckily, I know how to calculate a classpath: clj -Spath. In fact, my build-deps code is already using for its side effect of downloading dependencies, so all I have to do is capture its output. I already noticed that babashka.deps/clojure is printing the classpath to standard output, so if I wrap this in a with-out-str, I'm all good:

(let [classpath
      (with-out-str
        (clojure ["-Spath"]
                 {:dir work-dir
                  :env (assoc (into {} (System/getenv))
                              "GITLIBS" (str gitlibs-dir))}))]
  (println "Classpath:" classpath)
  ;; ...
)

Running bb build-deps --deps-path ../s3-log-parser/src/bb.edn, I see:

Classpath: src:~/.work/m2-repo/com/cognitect/aws/endpoints/1.1.12.206/endpoints-1.1.12.206.jar:...

and so on. Cool! Now I have a classpath. The only problem with this is that it is an absolute path to my .work directory, and my dependencies are going to end up in /opt when I add my deps layer to my lambda. So I'll need to rewrite the classpath to say /opt everywhere it currently says ~/.work. No worries, Clojure can do that:

(let [deps-base-dir (str (fs/path (fs/cwd) work-dir))
      classpath
      (with-out-str
        (clojure ["-Spath"]
                 {:dir work-dir
                  :env (assoc (into {} (System/getenv))
                              "GITLIBS" (str gitlibs-dir))}))
      deps-classpath (str/replace classpath deps-base-dir "/opt")]
  (println "Classpath before transforming:" classpath)
  (println "Classpath after transforming:" deps-classpath)
  ;; ...
)

Running this gives me what I'm looking for:

Classpath before transforming: src:~/.work/m2-repo/com/cognitect/aws/endpoints/1.1.12.206/endpoints-1.1.12.206.jar:...
Classpath after transforming: src:/opt/m2-repo/com/cognitect/aws/endpoints/1.1.12.206/endpoints-1.1.12.206.jar:...

Victory! 🎉

The next step is to pass that classpath along to Blambda so that it can set it when invoking bb in the custom runtime. I decided to write the classpath to a file that can be included in the deps layer, which Blambda will read when initialising the runtime:

(let [classpath-file (fs/file work-dir "deps-classpath")
      deps-base-dir (str (fs/path (fs/cwd) work-dir))
      classpath
      (with-out-str
        (clojure ["-Spath"]
                 {:dir work-dir
                  :env (assoc (into {} (System/getenv))
                              "GITLIBS" (str gitlibs-dir))}))
      deps-classpath (str/replace classpath deps-base-dir "/opt")]
  (println "Classpath before transforming:" classpath)
  (println "Classpath after transforming:" deps-classpath)
  (spit classpath-file deps-classpath)

  (println "Compressing custom runtime layer:" deps-zipfile)
  (let [{:keys [exit err]}
        (sh "zip" "-r" deps-zipfile
            (fs/file-name gitlibs-dir)
            (fs/file-name m2-dir)
            (fs/file-name classpath-file)
            :dir work-dir)]
    (when (not= 0 exit)
      (println "Error:" err))))

When the deps layer is unzipped on the lambda instance, I'll have a file named /opts/deps-classpath containing the transformed classpath. Now all I have to do is update Blambda's bootstrap script to use the file if it's there:

#!/bin/sh

set -e

LAYERS_DIR=/opt
DEPS_CLASSPATH_FILE="$LAYERS_DIR/deps-classpath"

CLASSPATH=$LAMBDA_TASK_ROOT
if [ -e $DEPS_CLASSPATH_FILE ]; then
  CLASSPATH="$CLASSPATH:`cat $DEPS_CLASSPATH_FILE`"
fi

export BABASHKA_DISABLE_SIGNAL_HANDLERS="true"
export BABASHKA_PODS_DIR=$LAYERS_DIR/.babashka/pods
export GITLIBS=$LAYERS_DIR/gitlibs

echo "Starting Babashka:"
echo "$LAYERS_DIR/bb -cp $CLASSPATH $LAYERS_DIR/bootstrap.clj"

$LAYERS_DIR/bb -cp $CLASSPATH $LAYERS_DIR/bootstrap.clj

Now the classpath will always start with $LAMBDA_TASK_ROOT, which is the directory where the lambda zipfile is extracted (/var/task), and then if there exists an /opt/deps-classpath file, its contents will be appended to the classpath.

Invoking bb with the -cp flag overrides the default classpath and stops Babashka from building the classpath from bb.edn. This also means that there's no need to include a bb.edn in the lambda archive, which saves precious bytes! 😉

After running bb deploy to deploy the new version of the Blambda custom runtime and creating a new version of my deps layer with target/deps.zip, I held my breath and clicked the Test button once again:

The lambda console showing a successful test result

Victory! 🎉

Of course, there's a lot that is pretty yucky about this. The yuckiest bit is probably that I have to build and deploy Blambda from the Blambda repo, build the deps layer from the Blambda repo, but deploy the deps layer and build and deploy the lambda archive itself from the s3-log-parser repo. Gross. Will fix.

Discuss this post here.

Published: 2022-08-09

Tagged: clojure aws blambda s3 babashka lambda

Here we go again!

It's the beginning of August and Arsenal's Premier League campaign is starting again tonight with a match against Crystal Palace. This is definitely the most excited I've been about a new season in quite a few years!

Gabriel Magalhães, Gabriel Martinelli, and Gabriel Jesus standing together in
white Arsenal training tops

The start of the first Emery season was interesting because for the first time in 20+ years, no one had any idea what to expect, and the start of the second Emery season was exciting because we'd just signed some Ivorian winger from the French league for £72 million, so surely he'd be awesome, right? What was his name again? The start of the first full Arteta season was exciting because we'd just won the Arsène Wenger Memorial Cup for the 14th time and surely we'd kick on from there, right?

In contrast, the start to last season was really low key for me. My expectations were that we'd finish somewhere around 6th, and a good target for the season would be qualifying for the Europa League. We'd signed Ødegaard permanently, and that was awesome, but I didn't expect he'd single-handedly turn us into top four challengers. We signed some young English defender named Ben White that I'd never even heard of, and £50 million was a lot of bloody money and last time we spent that on a player it was Pépé and we all remember how that went, right? And we had a tough start to the season! The opener against Brentford was an easy three points, sure, but then we faced Chelsea and City, and if we came out of those matches with even a point, it would be a miracle.

And then the season actually started and it was worse than even I expected. Taking 0 points from the first 9 and scoring no goals in the process was... bad. Very bad. I was very angry at Tim from 7amkickoff for being right as usual about Arteta not being a very good manager, but then things got less bad with a couple of 1-0 to the Arsenal wins and then holy shit we destroyed Spurs in the NLD and went on an unbeaten run that eventually lasted 10 games, but then we lost three in a row again and then we did that thing where we realise the season is halfway over and start getting results, and there were signs that Arteta was learning from his mistakes and then we were somehow in the hunt for the Champion's League and then we lost three in a row again and shit we were gonna finish out of the CL places and below Spurs again but then we won four in a row WTF we're gonna do it but then we lost two and finished below Spurs but at least destroyed Everton on the last day of the season to leave a smile on the home fans' faces.

Wow, last season was an absolute rollercoaster, wasn't it? And in checking my memory against the results from last season, I realised that we only drew three games all season. That's a bit astonishing to me. I don't think it means anything important, but just seems really unusual. At the end of the season, I was disappointed that we couldn't quite get over the last hurdle and qualify for the Champions League again, and of course sad that another year would go by without a St. Totteringham's Day celebration, but on the other hand, we had finished higher than I expected before the season started, and we were back in Europe and it wasn't the Conference League, so honestly, it was a decent season for me. 6/10, in the final analysis.

This summer, we've brought in a centre forward who can hustle and bustle and press and pass and most importantly, score! We've brought in really solid cover / competition for Kieran Tierney at left back in Zinchenko, we've finally found the new Viera that we've been looking for all these years, and we have William Saliba back from a great season in France where he won Young Player of the Year, made Team of the Year, and caught Mbappe from behind to take the ball cleanly and prevent a sure goal. In addition to being a really exciting young defender with great long passing, Saliba gives us the ability to move Ben White to right back when Tomiyasu is out (as he is right now).

I've only watched the highlights from the pre-season matches, as most of them were in the exceedingly early AM for me and then the one against Sevilla that I could have watched was ironically not available for streaming in Spain, where I was at the time, but wow oh wow am I ever excited! An Arsenal team successfully implementing a high press? An Arsenal team that can counter attack? An Arsenal team with a centre forward that is in the box and whacking the ball in the net? Yes please!

So I'm really pumped for tonight!

Discuss this post here.

Published: 2022-08-05

Tagged: arsenal

Archive